GitHub Advisoryadvisory · Packagist

GHSA-v383-3rw5-q8rf enshrined/svg-sanitize: Denial of Service via DTD Attribute Declaration Crash

MEDIUM Published 2026-10-08 Modified 2026-10-08 Official page ↗
Aliases
CVE-2026-107379
Ecosystem
Packagist
Packages
enshrined/svg-sanitize
Severity
Moderate
CVSS vector
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H
Weaknesses
CWE-770
Reviewed
2026-10-08

Affected packages

• Packagist enshrined/svg-sanitize: < 1.0.0 → fixed in 1.0.0

Description

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($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 ]> ``` Standalone reproduction ```php sanitize($svg); echo "Sanitized: " . strlen($clean) . " bytes\n"; // Process crashes at exit: munmap_chunk(): 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 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: ```php // In Sanitizer::sanitize(), before loadXML(): $dirty = preg_replace('/]*(?:\[.*?\])?\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

Related

Alias · CVE (1)

Weakness · CWE (1)

  • CWE-770 Allocation of Resources Without Limits or Throttling weakness

References