Wednesday, October 28, 2026

svg-sanitizer for stripping XSS payloads out of untrusted SVG uploads

svg-sanitizer for stripping XSS payloads out of untrusted SVG uploads

svg-sanitizer is a PHP library that filters untrusted SVG/XML content against strict whitelists, neutralizing XSS vectors in user-supplied files within authorized applications and defensive research.

Tooldarylldoyle/svg-sanitizer — PHP SVG/XML sanitizer filtering tags, attributes, and remote references
CategoryPHP security library / input sanitization
Primary UseCleaning user-uploaded or embedded SVG files before rendering, preventing script injection and XSS in web applications
Safe UseIntended for developers and authorized security teams hardening their own applications, upload pipelines, and CMS deployments such as WordPress, Drupal, and TYPO3
Telemetry NotePure server-side library; leaves no attacker-visible footprint. getXmlIssues() returns structured parse/sanitize errors useful for defensive logging and upload-audit trails

svg-sanitizer from darylldoyle/svg-sanitizer is a PHP library built around one narrow, well-defined problem: user-supplied SVG files are executable attack surface, not images. Because SVG is XML that browsers parse as live documents, an uploaded avatar or logo can carry <script> blocks, event-handler attributes, remote references, and CSS-based exfiltration channels. The library's author openly states the design is largely borrowed from DOMPurify, the cure53 JavaScript sanitizer, so the philosophy is allowlist-based filtering through a real DOM parse rather than regex mangling or blocklist guessing.

The runtime model is simple and explicit. You instantiate enshrined\svgSanitize\Sanitizer, hand your dirty markup to sanitize(), and get back either a cleaned SVG/XML string or boolean false when parsing fails. That boolean contract matters operationally: a malformed file never silently passes through, it is refused, and getXmlIssues() explains why. That makes it straightforward to build honest upload UX and defensive logging without guessing at failure causes.

A particularly thoughtful part of the README is the list of pre-parse rejections. Input is refused before the XML parser ever sees it if any element carries more than 4,096 attributes, if an internal DTD arrives in UTF-16 or UTF-32, if the encoding's markup is not plain ASCII (EBCDIC, UTF-7), or if the <svg> root sits in a namespace other than SVG. Each of these is a known parser-abuse lane, and refusing them at the boundary is far cheaper than letting a hostile document reach libxml and hoping for the best. This reads like hardening earned from real-world hostile input rather than speculative design.

The allowlists are customizable through Sanitizer::setAllowedTags and Sanitizer::setAllowedAttrs, which require implementing enshrined\svgSanitize\data\TagInterface or AttributeInterface respectively. The documentation makes a sharp operational point about <use>: it is allowed by default, but constrained by useThreshold() limiting reference expansion and setUseNestingLimit() limiting nesting depth, because <use>-based entity expansion is a classic SVG denial-of-service and billion-laughs-adjacent vector. If you inline untrusted SVG directly into page markup, the README's advice is to drop use from the allowlist entirely and eliminate that attack surface wholesale — guidance that shows the author understands where the library gets deployed.

Remote reference stripping is opt-in via $sanitizer->removeRemoteReferences(true). Enabled, it removes attributes pointing at remote files, which blocks HTTP leaks but costs sanitizer overhead. Notably, the CSS handling is described with unusual honesty: inside <style> elements and in style attributes, remote references in url(), @import, image-set(), and quoted strings starting with a remote URL are stripped, and srcset attributes are removed entirely because they are URL lists. The README explicitly calls this best-effort, not a full CSS parser, and frames it as defence-in-depth — telling operators to isolate untrusted CSS at the embedding point regardless. That candor about limits is a mark of a mature security project.

One documented behavioral quirk worth knowing before production deployment: when a CSS block loses a remote reference, any escape sequences in that same block are normalized (decoded) in the output. The result is semantically identical CSS, but hand-authored escapes get rewritten. Anyone diffing sanitized output against input, or relying on exact CSS byte preservation, should account for this. It is exactly the kind of detail that surfaces as a confusing bug ticket months later if it is not read now.

getXmlIssues() returns an array of everything the sanitizer observed, capped at 1,000 entries with a final entry noting how many were omitted. For blue teams and application engineers this is the observability hook: pipe it into your logging pipeline and you get a record of exactly what malformed or hostile constructs walked up to your upload endpoint, which is cheap threat intelligence about who is probing your file-handling surface. There is also $sanitizer->minify(true) for compacting output when storage or transfer size matters.

The ecosystem adoption is arguably the strongest trust signal in the whole context. The library ships as enshrined/svg-sanitize via composer, and it is wrapped by the safe-svg plugin in the official WordPress plugin directory for sanitizing uploads. A Drupal module exists at svg_sanitizer, and — most significantly — TYPO3 core has used this sanitizer by default since TYPO3 v9. When a CMS vendor bakes a library into core, that represents sustained review and real-world hostile-input exposure, which is more assurance than most GitHub security tools ever receive. The repo currently sits at 554 stars, is written in PHP, and is licensed under GPL-2.0 with CI badges and code-coverage tracking visible in the README.

For batch or assessment work there is a standalone CLI scanner contributed via a community PR, invoked as php svg-scanner.php ~/svgs/myfile.svg, with errors emitted as JSON. That turns the library from an embed-only dependency into something an authorized assessor can sweep a directory of collected SVG assets with, triaging which files would survive sanitization and which would be refused — useful when auditing an existing upload corpus during a sanctioned engagement. There is also a public demo linked from the README for interactive experimentation, and the test suite runs via vendor/bin/phpunit.

Installation is standard composer fare: composer require enshrined/svg-sanitize, then use enshrined\svgSanitize\Sanitizer; in your upload handler. The basic flow is file_get_contents() on the upload, one sanitize() call, and act on the string-or-false result. The README's to-do section candidly notes that more extensive SVG/XML test coverage is wanted, which is the right reading of the project: solid, widely deployed, but one where contributed test cases are explicitly welcomed.

From a defensive engineering perspective, this is a textbook example of doing input sanitization correctly: parse with a real XML parser, filter against allowlists derived from a proven project (DOMPurify), refuse pathological encodings up front, bound reference expansion, and expose diagnostics. It does not try to be a general HTML sanitizer, and it does not pretend its CSS handling is bulletproof. For any PHP application accepting SVG uploads — avatars, logos, diagram imports — this is the kind of control that removes an entire class of stored-XSS findings before a pentest ever finds them.

Official project repository for darylldoyle/svg-sanitizer.
Download Tool

Educational analysis for authorized security professionals. Use only in controlled, authorized environments.

Share articleFacebookXLinkedIn

Continue exploring

Browse all articles →

0 comentários:

Post a Comment

Note: Only a member of this blog may post a comment.