Sep 30, 2026·8 min read·4 visits
DOMPurify versions prior to 3.4.16 are vulnerable to DOM XSS when using in-place sanitization with custom hooks. If a hook detaches an element during the afterSanitizeElements or afterSanitizeAttributes phases, nested payloads avoid sanitization and execute in the browser.
A DOM-based Cross-Site Scripting (DOM XSS) vulnerability in DOMPurify allows attackers to bypass sanitization when using the in-place sanitization configuration (IN_PLACE: true) combined with custom hooks. If custom hooks detach elements during post-processing phases, nested malicious payloads can escape sterilization and execute in the browser's context.
DOMPurify is a client-side HTML sanitization library designed to mitigate Cross-Site Scripting (XSS) vulnerabilities. By default, the library parses dirty HTML markup inside an inert, detached DOM context, such as a DocumentFragment, to prevent premature resource resolution or execution of malicious handlers. Once sanitization is complete, the sanitized node is either converted back to a safe HTML string or imported into the target document safely.
To optimize performance in intensive client-side environments, DOMPurify offers an alternative execution configuration known as in-place sanitization (IN_PLACE: true). When this setting is enabled, DOMPurify performs sanitization directly on a live DOM tree passed by the application, modifying elements in memory. This option is popular in complex single-page applications because it completely bypasses the processing overhead associated with repeated DOM serialization and deserialization.
However, this optimization introduces a critical attack surface when combined with custom sanitization hooks. If an application registers custom hooks that programmatically detach elements from the active document structure, the sanitization engine's traversal mechanics are disrupted. This structural disruption allows nested malicious elements within the detached branch to escape validation entirely.
This vulnerability is cataloged as GHSA-P98J-92PF-MC4P and mapped to CWE-79 (Improper Neutralization of Input During Web Page Generation). Because it has no corresponding CVE record, it escapes detection by traditional vulnerability scanners and automated security audit tools, making manual technical analysis and targeted remediation essential for secure application engineering.
The root cause of GHSA-P98J-92PF-MC4P lies in the interaction between the browser's native tree traversal APIs, custom hook callbacks, and DOMPurify's post-sanitization lifecycle phases. DOMPurify relies on a native NodeIterator object, initialized via document.createNodeIterator, to walk the targeted DOM tree. The iterator flatly and sequentially visits every node in the DOM tree, executing security validations on each element.
To allow developers to implement custom policies, DOMPurify exposes multiple extensibility hooks that execute at distinct stages of element processing. These hooks include beforeSanitizeElements, uponSanitizeElement, and the post-sanitization phases afterSanitizeElements and afterSanitizeAttributes. If a custom hook callback detaches a node from the active tree (for example, by invoking node.remove() or parentNode.removeChild()), the parent-child relationship of that subtree is broken.
When a node is detached, the browser's NodeIterator automatically advances its internal pointer past the detached element. Consequently, the descendants of the detached node are never visited by the remaining iterations of the sanitization loop. This results in a complete bypass of DOMPurify's validation routine for all children nested within the detached parent node.
Because the sanitization operates on a live DOM tree, the browser's rendering engine may have already parsed the untrusted markup and scheduled asynchronous events before the sanitizer is invoked. An element such as <img src="invalid" onerror="payload"> schedules its onerror callback in the browser's event queue immediately upon parsing. If the container element is detached during a hook phase and its nested descendants escape neutralization, the browser processes the queued event handler once synchronous execution completes, triggering the XSS payload.
Prior to version 3.4.16, DOMPurify maintained a defensive mechanism called _handleHookDetachedNode. This function recursively invokes _neutralizeSubtree to strip all attributes and event handlers from any detached node, neutralizing potential threats. However, this safety function was only integrated during the early phases of node processing: beforeSanitizeElements and uponSanitizeElement. Detachments occurring during the post-processing hook phases bypassed this defense entirely.
The security patch introduced in commit b9b9d80f7e401771c2ccaef5f45def7eec8f27d7 resolves this gap by integrating _handleHookDetachedNode into the exit paths of the afterSanitizeElements and afterSanitizeAttributes hooks. This ensures that any detachment occurring at the end of element or attribute processing triggers immediate neutralization of the detached subtree before the browser returns control to the event loop.
// Patch in src/purify.ts fixing the afterSanitizeElements block
if (removed === false) {
_executeHooks(hooks.afterSanitizeElements, currentNode, null);
/* The hook may have detached the kept custom element. Same
IN_PLACE hazard as the before/upon sites: neutralize the
detached subtree before the walker skips past it (see
_handleHookDetachedNode). */
if (_handleHookDetachedNode(currentNode, root)) {
return true;
}
}Additionally, the patch addresses potential bypasses within the attribute sanitization routines. In _sanitizeAttributes, a custom hook could detach the target node, which would previously go undetected. The patched implementation checks for node detachment both before and after executing attribute-focused hooks, establishing complete lifecycle coverage.
const _sanitizeAttributes = function (
currentNode: Element,
root: Node
): void {
/* Execute a hook if present */
_executeHooks(hooks.beforeSanitizeAttributes, currentNode, null);
/* A hook may have detached the node - the attribute hooks can detach
exactly like the element hooks can, and the walker will not revisit
the detached subtree. Treat it as removed and, on the IN_PLACE path,
neutralize it (see _handleHookDetachedNode for the full rationale). */
if (_handleHookDetachedNode(currentNode, root)) {
return;
}
// ...To successfully exploit this vulnerability, three distinct conditions must be met. First, the application must invoke DOMPurify with the { IN_PLACE: true } configuration option. Second, a custom hook callback must be registered that conditionally removes or detaches elements during the afterSanitizeElements or afterSanitizeAttributes lifecycle phases. Third, the application must allow user-controlled markup to be appended to a live DOM element before invoking the sanitizer.
The exploit workflow begins when an attacker-crafted payload is injected into the application. The payload is carefully nested, ensuring that a benign outer element matches the deletion logic of the custom hook, while a malicious inner element contains the active payload.
<div id="remove-me">
<img src="does-not-exist" onerror="alert('DOM XSS')">
</div>When the application sets the innerHTML of a live container with this payload, the browser parsing engine instantly queues the image's onerror event handler. Next, the application calls DOMPurify.sanitize(element, { IN_PLACE: true }). During the walk, the custom hook identifies <div id="remove-me"> and detaches it. Since the detachment happens in the post-processing hook, the inner <img> is skipped by the walker and escapes sanitization. Once the synchronous execution context of the sanitizer returns, the browser executes the queued event handler in the application context.
Below is a conceptual visual representing the traversal bypass:
The security impact of GHSA-P98J-92PF-MC4P is severe, resulting in unauthenticated arbitrary JavaScript execution in the browser of the victim. This execution occurs within the context of the vulnerable application's origin. Consequently, the attacker can access sensitive resources, session tokens, and localStorage variables, and perform actions on behalf of the victim.
In modern single-page applications, DOM XSS allows attackers to perform session hijacking, exfiltrate JSON Web Tokens (JWTs), and bypass anti-CSRF protections. Attackers can also inject credential harvesting fields or redirect users to malicious third-party destinations. Because the exploit runs under the trusted domain name, users are highly susceptible to social engineering or phishing tactics integrated into the hijacked session.
The vulnerability is assessed with a CVSS 3.1 score of 8.1 (High) using the vector CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:N. Although the impact is critical, the exploitability is constrained by the requirement for specific application-defined hooks and configuration options, which lowers the probability of widespread automated exploitation but remains highly dangerous for targeted installations.
The primary remediation path is upgrading the dompurify dependency to version 3.4.16 or higher. This version ensures that any elements detached during post-processing hooks are systematically neutralized, preventing event handlers inside the detached subtree from executing.
npm install dompurify@3.4.16If upgrading is not immediately possible, developers can implement architectural workarounds. Custom hooks should avoid calling raw detachment methods like node.remove() or node.parentNode.removeChild(node). Instead, applications should leverage DOMPurify's native, declarative configuration blocklists and whitelists, such as FORBID_TAGS and FORBID_ATTR to direct the parser without breaking the traversal mechanics.
// Safe configuration bypasses custom node-removal hooks
DOMPurify.sanitize(dirtyElement, {
FORBID_TAGS: ['div'],
FORBID_ATTR: ['id'],
IN_PLACE: true
});Additionally, applications should restrict the use of { IN_PLACE: true } to scenarios where performance testing demonstrates a strict necessity. Operating on inert document contexts or handling inputs as raw HTML strings prior to injection provides strong defense-in-depth, neutralizing the event-loop race condition that enables this class of DOM XSS.
CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
dompurify Cure53 | < 3.4.16 | 3.4.16 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-79 |
| Attack Vector | Network (Client-Side DOM XSS) |
| CVSS Score | 8.1 (High) |
| Exploit Status | Proof-of-Concept (PoC) |
| KEV Status | Not Listed |
| Impact | Arbitrary Client-Side Code Execution |
Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
A security vulnerability in league/commonmark versions 1.3.0 through 2.10.1 allows remote attackers to bypass Stored Cross-Site Scripting (XSS) protections in the DisallowedRawHtml extension. Due to an validation logic flaw in the regular expression parser, specifically handling bare, unclosed HTML blocks ending at the string boundary, raw HTML tags can be passed to the rendered output. When combined with browser-side parsing heuristics, an attacker can execute arbitrary JavaScript in the context of the user session.
A security vulnerability in serialize-javascript v7.1.1 allows Cross-Site Scripting (XSS) due to an overly greedy regular expression (SCRIPT_CLOSE_REGEXP) used during function serialization. Two secondary defects involving a spoofed toString() validation bypass and a stateful native code validator are also addressed in the fixed version v7.1.2.
A Denial of Service (DoS) vulnerability exists in the PyJWT library when parsing unverified token payloads containing deeply nested JSON structures. Because PyJWT fails to catch RecursionError during payload parsing, an unauthenticated remote attacker can crash the application thread or worker by sending a specially crafted token.
An improper output encoding and escaping vulnerability (CWE-116) in Vercel Satori allows unauthenticated remote attackers to perform markup injection in dynamic Open Graph images generated via Next.js's ImageResponse. Unsanitized parameter interpolation into SVG elements breaks XML structural boundaries. This exposes downstream parsing, rasterization, and rendering pipelines to Server-Side Request Forgery (SSRF), Local File Read, and Remote Code Execution (RCE).
An uncontrolled recursion vulnerability exists in PyJWT from version 2.13.0 to 2.14.0. The vulnerability allows remote, unauthenticated attackers to cause a Denial of Service (DoS) via crafted JWT headers that trigger stack exhaustion during JSON decoding.
A signature verification bypass vulnerability in PyJWT allows unauthenticated remote attackers to forge JSON Web Tokens when processing JSON Web Key Sets containing an empty symmetric key.