GHSA-cg2g-xh64-v4q7
DOMPurify SAFE_FOR_TEMPLATES bypass: split template expressions survive in attached shadow root content on DOM output modes
Summary
## 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 ` .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 .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 ` ` 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 = docum
Affected packages
| Ecosystem | Package | Affected versions | Fixed versions |
|---|---|---|---|
| npm | dompurify | — | 3.4.17 |
Remediation: Upgrade to 3.4.17 or later.
References
Includes data from the GitHub Advisory Database, licensed under CC-BY 4.0.