A AegiFlow
LOWCVSS 2.3

GHSA-p98j-92pf-mc4p

DOMPurify: IN_PLACE: node-removing afterSanitize hook leaves detached subtree event handlers armed, causing DOM XSS

Published
2026-09-30
Modified
2026-09-30
Sources
github-advisory

Summary

## Summary In `IN_PLACE` mode DOMPurify sanitizes the caller's live DOM subtree directly. To close a known hazard (GHSA-55q2-fjhq-7xh7), the library neutralizes any node a hook detaches during sanitization by stripping its subtree's non-allow-listed attributes, but this neutralization is wired only into the `beforeSanitizeElements` and `uponSanitizeElement` hook sites. An `afterSanitizeElements` or `afterSanitizeAttributes` hook that removes a non-root element (a documented, supported pattern) detaches that element's subtree with no neutralization, so descendant `on*` handlers remain armed on the caller's live tree after `sanitize()` returns, yielding DOM XSS. No special privilege is required beyond supplying markup to an application that uses `IN_PLACE` together with a node-removing afterSanitize hook. ## Root Cause `IN_PLACE` sanitization mutates the caller's live document, so any element a hook detaches from that tree must have its subtree neutralized before `sanitize()` returns; otherwise a queued resource-event handler (for example an ` ` that began loading when the caller built the tree) fires in page scope even though the handler never reached the sanitized output. The guard helper `_handleHookDetachedNode` (which calls `_neutralizeSubtree` in `IN_PLACE`) is invoked only after `beforeSanitizeElements` and after `uponSanitizeElement`. It is never invoked from the afterSanitize return paths, and `_sanitizeAttributes` never calls it at all. The post-walk `IN_PLACE` neutralization pass iterates only `DOMPurify.removed`, and hook-detached nodes are intentionally not recorded there, so that pass cannot reach them either. ```text src/purify.ts _sanitizeElements: _handleHookDetachedNode present at lines 2142 and 2175 (before/upon), absent after afterSanitizeElements at lines 2208 and 2249. _sanitizeAttributes: afterSanitizeAttributes fires at line 2670 with no detach re-check; the function never calls _handleHookDetachedNode. Post-walk IN_PLACE pass (lines 3081-3090) iterates DOMPurify.removed only, which by design (comment at lines 2096-2099) excludes hook-detached nodes. ``` ## Impact An attacker who supplies markup processed by a victim application that runs `DOMPurify.sanitize(node, { IN_PLACE: true })` and registers a node-removing `afterSanitizeElements` or `afterSanitizeAttributes` hook can retain arbitrary `on*` event handlers on descendants of a removed non-root element. Because `IN_PLACE` operates on the caller's live document, a queued resource-event handler on such a descendant fires in the page origin after the synchronous `sanitize()` call returns, giving script execution in the victim's session (DOM XSS). This defeats DOMPurify's `IN_PLACE` contract to neutralize handlers on subtrees removed from the live tree. The capability is script execution in the victim origin; exact confidentiality and integrity effects depend on the hosting application's session. ## Proof of Concept ```text Dependencies: Node.js and jsdom. The harness builds a live tree, registers a removal hook, runs IN_PLACE sanitize(), then inspects whether the attacker onerror handler survives on the detached descendant. jsdom does not perform real image loads, so the retained handler (rather than an actual fired event) is the observed hazard; the retained on* attribute on a live detached node after a synchronous sanitize() return is the neutralization failure. ``` Reproduction steps: 1. Build a live subtree `#root > section#wrap > img[onerror]` where `#root` is the walk root and `section#wrap` is a non-root wrapper. 2. Register an `afterSanitizeElements` (or `afterSanitizeAttributes`) hook that calls `node.remove()` on the wrapper. 3. Call `DOMPurify.sanitize(root, { IN_PLACE: true })`. 4. Observe that the descendant ` ` retains its `onerror` handler, whereas the same removal performed in a `beforeSanitizeElements`/`uponSanitizeElement` hook strips it. ```javascript const { JSDOM } = require('jsdom'); const

Affected packages

EcosystemPackageAffected versionsFixed versions
npmdompurify—3.4.16

Remediation: Upgrade to 3.4.16 or later.

References

Includes data from the GitHub Advisory Database, licensed under CC-BY 4.0.