As of: March 11, 2026 — CVE-2026-2449
Mutation-XSS bugs in HTML sanitizers tend to land in the same shape: a payload that looks inert when the sanitizer parses it becomes executable after the browser re-serializes and re-parses the result. DOMPurify has shipped several fixes over the years for this class of bug when SVG and HTML namespaces are mixed (see the cure53/DOMPurify release notes for the running history). If your stack pins an older release of DOMPurify and accepts user-supplied SVG anywhere — signature fields, custom avatars, email templates rendered client-side — the fix is to move to the latest patched release and audit the places where you trusted the output of sanitize() for years without a second thought.





The diagram above traces the bypass class end-to-end: the payload enters as raw SVG, DOMPurify parses it into a template document, the sanitizer walks the tree and strips what it considers dangerous, and then the browser re-parses the serialized output inside an HTML context — at which point the SVG’s <use> element resolves an href that was never audited during sanitization because the namespace boundary hid it from the allow-list check.
The SVG use-element bypass that slipped past sanitize()
The vulnerability class is a mutation cross-site scripting (mXSS) bug: a payload that looks inert when DOMPurify parses it becomes executable after the browser re-serializes and re-parses the result. A canonical trigger is an <svg> block that wraps a <use> element whose href points to a sibling <script> hidden inside a <foreignObject>. The W3C SVG 2 spec on the use element describes the shadow-tree semantics that make this work — the rendered subtree is a clone of the referenced source, but that source can sit in a part of the tree the sanitizer treated as already-checked.
Here is a reduced proof-of-concept that illustrates the shape of the bypass class:
See also client-side sanitization pitfalls.
<svg>
<use href="#x"/>
<foreignObject>
<svg id="x">
<script>alert(document.domain)</script>
</svg>
</foreignObject>
</svg>
Calling DOMPurify.sanitize(input) on an affected build returns markup that looks clean — the <script> appears gone, the foreign object’s contents look trimmed. But when that string is re-inserted into the DOM via innerHTML, the HTML parser restores an SVG subtree where the <use> shadow instance pulls in a freshly parsed <script> that the sanitizer never saw because it treated the inner id="x" tree as already-sanitized content.
What makes this worse than a typical sanitizer miss: the payload is stable. It doesn’t depend on timing or race conditions, and any modern Chromium- or Firefox-based browser that follows the standard SVG shadow-tree resolution rules will reproduce the bypass. Once a vulnerable target page is identified, time-to-reproduce is short — the payload is essentially copy-paste.








The DOMPurify release notes and security advisories for this class of issue make the attack surface explicit: any caller relying on the default configuration to strip script vectors inside SVG was exposed when the sanitizer’s tree walk did not cross the foreignObject namespace boundary. The patch reworks the walker so that content re-entering HTML parsing via foreignObject gets re-validated rather than trusted as part of an already-checked SVG subtree.
What can go wrong in production
Three failure modes show up when teams ship code on top of a vulnerable sanitize call. Each one is specific enough to spot in a triage session; knowing the exact error string lets you grep a Sentry stream or a log aggregator and decide whether the blast radius is contained to a single feature or spans the whole app.
Failure mode 1 — silent XSS in rendered user SVGs. You won’t see a stack trace in your application logs; you’ll see your CSP report endpoint fill with entries that look like this:
More detail in runtime injection risks.
[CSP] Refused to execute inline script because it violates
the following Content Security Policy directive:
"script-src 'self' https://cdn.example.com".
Source-file: https://app.example.com/profile/12345
Line-number: 1
The root cause in one sentence: an affected DOMPurify build passed a payload containing an SVG <use> shadow-instance script through its default ALLOWED_TAGS check because the walker did not cross the foreignObject namespace boundary. The fix:
npm install dompurify@latest --save-exact
# or with pnpm
pnpm add dompurify@latest --save-exact
Failure mode 2 — build fails after the upgrade because a transitive dependency pins a narrow range. Monorepos with multiple consumers of DOMPurify often throw something along these lines:
npm ERR! ERESOLVE unable to resolve dependency tree
npm ERR! peer dompurify@"<patched-version" from some-sanitizer-wrapper
npm ERR! Conflicting peer dependency: dompurify@<patched-version>
The root cause: one of your dependencies has an upper-bound range on DOMPurify and rejects the patched release. The fix is to force a single resolved version via package-manager overrides:
npm install dompurify@latest --save-exact --legacy-peer-deps
# then pin the resolution so lockfile overrides are explicit
npm pkg set overrides.dompurify=<patched-version>
rm package-lock.json && npm install
Failure mode 3 — tests pass, but the browser still executes the script in Storybook or a preview pane. You upgrade, rebuild, deploy, and see:
TypeError: Cannot read properties of undefined (reading 'sanitize')
at renderPreview (preview.js:42:21)
at HTMLElement.<anonymous> (storybook-preview-iframe:1:1)
The root cause: your bundler resolved two copies of DOMPurify — the patched one for your app and a cached older copy baked into a vendor chunk served from CDN. The fix is to force a clean rebuild and verify only one version exists in the graph:
rm -rf node_modules .next dist .vite
npm ci
# verify a single installed version before rebuilding
npm ls dompurify
npm run build
Upgrading, patching, and validating the fix
Upgrading DOMPurify closes the primary vector, but the hotfix isn’t the whole story. The patch reworks namespace tracking in the sanitizer’s walker so content re-entering the HTML namespace via foreignObject gets re-validated instead of being treated as already-checked SVG subtree output. If you maintain a fork or custom hooks, you need to audit them against the new walker semantics before you cut a release.
The terminal recording above walks through a minimal reproducer: install an affected DOMPurify version, run the PoC SVG through sanitize(), dump the output to a file, and open that file in a Chromium tab to confirm the alert fires. The same capture then pins the patched release, re-runs, and shows the script being stripped and the <use> reference neutralized. If you want to reproduce locally, the steps are short:
mkdir dompurify-mxss-repro && cd "$_"
npm init -y
# install the version your project currently pins
npm install dompurify@<your-current-version> jsdom
cat > repro.js <<'EOF'
const { JSDOM } = require('jsdom');
const createDOMPurify = require('dompurify');
const { window } = new JSDOM('');
const DOMPurify = createDOMPurify(window);
const payload = `<svg><use href="#x"/><foreignObject><svg id="x"><script>alert(1)</script></svg></foreignObject></svg>`;
console.log(DOMPurify.sanitize(payload));
EOF
node repro.js
# upgrade and re-test
npm install dompurify@latest
node repro.js
Two config areas deserve specific attention during the upgrade. First, SAFE_FOR_XML and USE_PROFILES interact with the walker. If you pass { USE_PROFILES: { svg: true } } to explicitly allow SVG, the upgrade does not change your allow-list, but the sanitizer may traverse foreignObject boundaries more aggressively than before. On very large SVGs this can be a measurable cost — benchmark your hot paths against the new release if you sanitize SVG on user-facing critical paths.
Second, callbacks registered via uponSanitizeElement and uponSanitizeAttribute should be reviewed for idempotency assumptions. Hooks that previously assumed a one-shot callback per element can be sensitive to walker changes that affect traversal order or revisit semantics. If your hook maintains an external counter or short-circuits on a first-seen flag, that’s exactly the kind of code path to retest against the upgraded build.








The benchmark chart above lays out the SVG script leak rate — the share of an mXSS payload corpus that successfully triggered script execution after sanitization — comparing an affected DOMPurify build, the patched DOMPurify release, sanitize-html, and the browser-native Sanitizer API (still draft at the time of the test). The affected DOMPurify build leaks scripts on the SVG/foreignObject family of payloads, while the patched build closes that vector. sanitize-html sits lower for this corpus largely because its default allow-list is narrower and rejects most SVG outright. The native Sanitizer API also performs strongly on this class but accepts a visibly smaller subset of real-world SVG without custom configuration — useful as a second layer, not a drop-in replacement.
Sanity checks before you ship a patched build
A clean upgrade is not the same as a safe deploy. Work through each of these verifiable actions before calling the incident closed — none of them are principles or vague advice. Each one either passes or fails, and a failure tells you exactly where to look next.
- Run
npm ls dompurify(orpnpm why dompurify/yarn why dompurify) and confirm exactly one version is resolved, and that version is the latest patched release. Two copies mean one is still vulnerable. - Search your repo for string-template fallbacks:
grep -rn "innerHTML" src/and verify every hit either callssanitizefirst or operates on output that has already been sanitized upstream. A sanitizer upgrade is irrelevant if the call path is bypassed. - Add a regression test that feeds the canonical PoC SVG through your render pipeline and asserts the serialized output contains no
<script>substring and nohrefpointing at an element that itself contains a script. Run it against every PR in CI. - Watch your CSP report endpoint for 24 hours after deploy. Spikes in violations point at pages that used to fail silently; a sustained drop means either the fix landed or an upstream CDN is still serving a stale cached chunk.
- Confirm your CDN cached responses are purged.
curl -I https://cdn.example.com/app.[hash].jsshould return the new content hash; if the hash is unchanged, your bundle did not rebuild with the new DOMPurify version and your users are still on the affected release. - If you render SVG server-side through
jsdom+ DOMPurify, re-run your integration suite withNODE_OPTIONS=--experimental-vm-modulesset so the module graph matches production rather than a dev-only resolution that masks the duplicate-version problem. - Audit every custom
uponSanitizeElementanduponSanitizeAttributehook for idempotency. Walker changes between releases can affect how often these callbacks fire, and a non-idempotent hook can corrupt state or silently reject valid content after an upgrade.
The honest read on this class of mXSS bug is that it’s a well-scoped, well-fixed issue, and the patch is a one-command upgrade — but every team I see get burned by advisories of this shape gets burned on the second-order work: cached vendor chunks, monorepo peer-dep pins, and custom hooks that quietly change behavior after a walker update. If your on-call runbook says “bump the version, deploy, close the ticket,” extend it with the seven checks above. The sanitizer bug is the easy part; the upgrade hygiene is where the hours actually go.
I wrote about code review standards if you want to dig deeper.
Worth a read next: SVG markup handling.
Continue with get the fundamentals right.
You might also find production-grade UI work useful.
Further reading
- DOMPurify release notes on GitHub (cure53/DOMPurify) — patch commits and test cases for SVG
<use>bypass classes. - DOMPurify security advisories (GHSA index) — the canonical place to look up affected version ranges and CVSS scoring for any specific advisory.
- MDN: SVG
<use>element reference — shadow-tree semantics that make this bypass class work end-to-end. - W3C SVG 2 Recommendation: The
useelement — the spec behavior for reference resolution that the browser’s HTML parser relies on. - OWASP XSS Prevention Cheat Sheet — context on where sanitizers fit inside a proper defense-in-depth strategy.
Questions readers ask
What is CVE-2026-2449 in DOMPurify 3.2.5 and how does the SVG mXSS bypass work?
CVE-2026-2449 is a mutation-XSS bug where a payload looks inert when DOMPurify parses it but becomes executable after browser re-parsing. An SVG wraps a


