Patch Forecast
Amber warning
Patch this week.

GHSA-cg2g-xh64-v4q7

DOMPurify SAFE_FOR_TEMPLATES bypass: split template expressions survive in attached shadow root content on DOM output modes

Risk score
70/100
CVSS
9.2 v4.0
Published
22 Oct 2026

Open in the live forecast Today's forecast

What to know

What it is
DOMPurify has a vulnerability when configured with SAFE_FOR_TEMPLATES: true and a DOM-returning mode, allowing an attacker to bypass sanitization and execute template expressions. An attacker can supply a DOM node with a shadow root to split an expression across text nodes, which will be evaluated by a downstream template engine.
Affected
{'products': ['DOMPurify'], 'packages': [{'ecosystem': 'npm', 'name': 'dompurify', 'vulnerable': '>= 3.4.4, <= 3.4.16'}], 'versions': ['3.4.4', '3.4.5', '3.4.6', '3.4.7', '3.4.8', '3.4.9', '3.4.10', '3.4.11', '3.4.12', '3.4.13', '3.4.14', '3.4.15', '3.4.16']}
Fix
Upgrade to version 3.4.17 or later
Exploitation
Public proof of concept

Not confirmed. 3.4.5, 3.4.6, 3.4.7, 3.4.8, 3.4.9, 3.4.10, 3.4.11, 3.4.12 isn't in the advisory. Check the vendor's advisory before acting on this.

Story map

Story map: dompurifyTHE FLAWSTHE FIXGHSA-cg2g-xh64-v4q7DOMPurifySAFE_FOR_TEMPLATES bypass:split template expressions…CVSS 9.2GHSA-mrv2-3pqr-j5jrDOMPurify IN_PLACE: hookthat removes a parent orsibling leaves the detached…CVSS 7.6FixUpgrade to version 3.4.17 orlaterWho is exposed{'products': ['DOMPurify'],'packages': [{'ecosystem':'npm', 'name': 'dompurify','vulnerable': '>= 3.4.4, <=3.4.16'}], 'versions': ['3.4.4'…AMBER · PATCH THIS WEEKdompurifyDOMPurify has a vulnerabilitywhen configured withSAFE_FOR_TEMPLATES: trueand a DOM-returning mode,allowing an attacker to bypasssanitization and executetemplate expressions. An…Where it standsPublic proof of conceptCONNECTS TOSame weaknessCross-site scripting (CWE-79)399 other warnings in 30 days →
Story map: the flaws, the fix, how it unfolded and what it connects to. Boxes link to the record or article.

At a glance

dompurify: Amber, patch this week. DOMPurify has a vulnerability when configured with SAFE_FOR_TEMPLATES: true and a DOM-returning mode, allowing an attacker to bypass sanitization and execute template expressions. An attacker can supply a DOM node with a shadow root to split an expression across text nodes, which will be evaluated by a downstream template engine.

The flaws

  • GHSA-cg2g-xh64-v4q7: DOMPurify SAFE_FOR_TEMPLATES bypass: split template expressions survive in attached shadow root content on DOM output modes (CVSS 9.2)
  • GHSA-mrv2-3pqr-j5jr: DOMPurify IN_PLACE: hook that removes a parent or sibling leaves the detached subtree's event handlers armed (incomplete fix for GHSA-p98j-92pf-mc4p) (CVSS 7.6)

The fix

Fix: Upgrade to version 3.4.17 or later

Who is exposed: {'products': ['DOMPurify'], 'packages': [{'ecosystem': 'npm', 'name': 'dompurify', 'vulnerable': '>= 3.4.4, <= 3.4.16'}], 'versions': ['3.4.4', '3.4.5', '3.4.6', '3.4.7', '3.4.8', '3.4.9', '3.4.10', '3.4.11', '3.4.12', '3.4.13', '3.4.14', '3.4.15', '3.4.16']}

Where it stands

  • Public proof of concept

Connects to

Why it is Amber

  • CVSS 9.2 critical
  • Public exploit or proof of concept referenced
  • Remotely exploitable without logging in
  • On your watchlist: npm

Affected packages

PackageEcosystemVulnerableFixed in
dompurifynpm>= 3.4.4, <= 3.4.163.4.17

References

Original description

## Summary When DOMPurify is configured with `SAFE_FOR_TEMPLATES: true` together with a DOM-returning mode (`RETURN_DOM: true`, `RETURN_DOM_FRAGMENT: true`, or `IN_PLACE: true`), template expressions such as `{{...}}`, `${...}`, and `<%...%>` survive sanitization inside the text content of an attached shadow root. The post-walk safety net `_scrubTemplateExpressions` (`src/purify.ts:1717`) was extended to recurse into `<template>.content`, but it never recurses into attached shadow roots. An attacker who can supply a DOM node carrying a shadow root (the `IN_PLACE` and shadow-aware `RETURN_DOM` surfaces) can split an expression across text nodes separated by disallowed elements; after those elements are removed the fragments become adjacent and serialize into a complete expression that a downstream template engine will evaluate. ## Root Cause `SAFE_FOR_TEMPLATES` neutralizes template expressions through two mechanisms: a per-text-node strip during the main walk (`_stripTemplateExpressions`, applied in `_sanitizeElements` at `src/purify.ts:2246`), and a final post-walk pass (`_scrubTemplateExpressions`, `src/purify.ts:1717`) that normalizes adjacent text nodes and re-strips expressions that only form after merging. The per-node strip cannot catch an expression that is split into individual fragments, because each fragment fails the delimiter regexes. The post-walk pass is the only mechanism that would catch the merged form, but it is structurally unable to reach shadow root content. ```text _scrubTemplateExpressions (src/purify.ts:1717-1751): - node.normalize() at line 1718 does not merge text nodes inside shadow roots (DOM spec). - NodeIterator at lines 1723-1731 does not descend into shadow trees (DOM spec). - Explicit recursion at lines 1743-1750 enters <template>.content only, via querySelectorAll('template'); there is no equivalent recursion into attached shadow roots. Shadow roots are sanitized earlier by _sanitizeShadowDOM (src/purify.ts:2710), which applies the per-node strip but never performs the post-walk normalize-and-rescrub. ``` Shadow root text nodes are therefore sanitized only per-node and never re-examined as a merged unit. ## Impact An attacker who controls DOM-node input containing an attached shadow root can inject template expressions that survive sanitization and are later evaluated by the application's template engine. In client-side contexts this yields cross-site scripting; in a server-side template context it can escalate to template injection. Exploitation requires non-default application configuration: the application must enable `SAFE_FOR_TEMPLATES` and a DOM-returning mode, pass a DOM node that already has an attached shadow root, and feed the sanitized shadow root content into a template evaluator. The shadow root does not have to be built programmatically with `attachShadow`: in engines that support declarative shadow DOM, attacker-supplied markup containing `<template shadowrootmode="open">` parsed through `setHTMLUnsafe` or `Document.parseHTMLUnsafe` produces the same attached shadow root, so a plain HTML string reaches the gap once the application is configured this way. Demonstrated capability is injection of a complete, attacker-chosen expression into post-sanitization shadow root content; the resulting script execution and any confidentiality or integrity consequence depend on the downstream engine's evaluation context. ## Proof of Concept ```text Preconditions: - DOMPurify built from the affected source (dist/purify.js or an ESM/CJS bundle). - A browser (or DOM implementation) with shadow DOM support. - Application constructs a DOM node with an attached shadow root before sanitizing, and later evaluates the shadow root's text content as a template. ``` Build a host element whose shadow root holds a template expression split across text nodes separated by disallowed elements, then sanitize in place with `SAFE_FOR_TEMPLATES`: ```javascript const host = document.createElement('div'); const root = host.attachShadow({ mode: 'open' }); // Split "${alert(document.domain)}" so no single fragment matches a regex. // <object> is not in the default ALLOWED_TAGS and is removed during sanitization, // leaving the surrounding text-node siblings adjacent. root.appendChild(document.createTextNode('$')); root.appendChild(document.createElement('object')); root.appendChild(document.createTextNode('{alert(document.domain)}')); DOMPurify.sanitize(host, { IN_PLACE: true, SAFE_FOR_TEMPLATES: true }); // After the disallowed element is removed, the fragments are adjacent and survive. host.shadowRoot.normalize(); console.log(host.shadowRoot.textContent); ``` ```text ${alert(document.domain)} ``` A downstream template evaluator reading `shadowRoot.textContent` (for example `new Function('return `' + content + '`')()` or any `${...}`/`{{...}}` engine) then evaluates the fully formed expression. ## Attack Chain 1. **Exposure:** The application calls `DOMPurify.sanitize(node, cfg)` (`src/purify.ts:2854`) with `SAFE_FOR_TEMPLATES: true` and `IN_PLACE: true` (or a shadow-aware `RETURN_DOM`), passing a DOM node that already carries an attached shadow root. 2. **Control:** The attacker controls the shadow root's text nodes and the disallowed separator elements between them; each fragment (`$`, `{alert(document.domain)}`) is a valid, individually benign text node. 3. **Path:** `_sanitizeAttachedShadowRoots` (`src/purify.ts:2977`) feeds the shadow root into `_sanitizeShadowDOM` (`src/purify.ts:2710`), which strips expressions per text node (`src/purify.ts:2246`) and removes the disallowed separator elements, leaving the fragments adjacent. 4. **Guard:** The post-walk safety net `_scrubTemplateExpressions` (`src/purify.ts:3131-3132`) is the only pass that would catch the merged expression, but it never recurses into shadow roots (`src/purify.ts:1717-1751` recurses into `<template>.content` only). 5. **Primitive:** The adjacent fragments seriali

Last activity 22 Oct 2026. Patch Forecast updates every hour.