GitHub Advisoryadvisory · Packagist
GHSA-9rjx-3jch-6vjf enshrined/svg-sanitize: Stored XSS via DTD Entity / HTML5 Named Character Reference Collision
MEDIUM
Official page ↗
- Aliases
- CVE-2026-107380
- Ecosystem
- Packagist
- Packages
- enshrined/svg-sanitize
- Severity
- Moderate
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N
- Weaknesses
- CWE-79
- Reviewed
- 2026-10-08
Affected packages
• Packagist enshrined/svg-sanitize: < 1.0.0 → fixed in 1.0.0
Description
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
```
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
```
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
]>
CLICK ME
```
Sanitizer processing
```php
sanitize($svg);
echo $clean;
```
Output:
```xml
CLICK ME
```
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
CLICK ME
```
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 ``). 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 `` 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('/]*(?:\[.*?\])?\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
Denis Rostilov
Related
Alias · CVE (1)
- CVE-2026-107380 savg-sanitizer is a PHP SVG/XML sanitizer. vulnerability
Weakness · CWE (1)
- CWE-79 Improper Neutralization of Input During Web Page Generation weakness
References
- https://github.com/darylldoyle/svg-sanitizer/security/advisories/GHSA-9rjx-3jch-6vjf web
- https://nvd.nist.gov/vuln/detail/CVE-2026-107380 advisory
- https://github.com/darylldoyle/svg-sanitizer/commit/23877db7e76f1e1df5c3e65ab30239219c3d2867 web
- https://github.com/darylldoyle/svg-sanitizer package
- https://github.com/darylldoyle/svg-sanitizer/releases/tag/1.0.0 web