Sep 30, 2026·8 min read·20 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')
CVE-2026-128087 (GHSA-9264-jvgf-6w2f) is an incorrect behavior order (CWE-696) and improper authorization (CWE-863) vulnerability in the russh Rust SSH library, affecting versions 0.55.0 through 0.65.0. During the initial SSH key exchange (KEX) handshake, the client state machine invokes the client::Handler::kex_done() callback—providing the negotiated raw shared secret bytes—prior to validating the remote server's host key or certificate via check_server_key(). Consequently, an untrusted or rogue SSH server can complete cryptographic key agreement and force a connecting client to execute side-effecting application logic inside kex_done() before the server's identity is checked and rejected.
CVE-2026-128082 (GHSA-6vhm-j9h7-f9gh) is an uncontrolled recursion vulnerability (CWE-674) in AutoMapper, a convention-based object-object mapper for .NET. When mapping nested or self-referential types into destination models that rely on parameterized constructors, C# positional records, or recursive ConstructUsing expressions, AutoMapper evaluates child constructor arguments prior to incrementing the parent's recursion depth counter. This ordering flaw bypasses both explicit MaxDepth limits and the default depth limit of 64. An unauthenticated remote attacker can supply deeply nested input payloads to affected endpoints, exhausting the thread call stack and triggering an uncatchable System.StackOverflowException that terminates the hosting .NET process.
An information exposure vulnerability in the @opentelemetry/instrumentation-http package for OpenTelemetry JavaScript versions 0.204.0 prior to 0.222.0 causes incoming HTTP server spans to record raw, unredacted query string parameters under the url.query attribute. While outgoing client spans applied built-in and user-configured query parameter redaction, incoming server spans bypassed this sanitization completely, resulting in the transmission of sensitive credentials such as pre-signed cloud URLs and API tokens to external observability backends.
CVE-2026-128089 is an uncontrolled resource consumption vulnerability in the dnspython toolkit for Python, affecting all versions prior to 2.9.2. When parsing Well-Known Service (WKS) records from text via dns.rdtypes.IN.WKS.WKS.from_text(), the parser fails to validate that numeric service port identifiers fall within the RFC 1035 valid range of 0 through 65535. An attacker capable of supplying untrusted zone files or RDATA text strings containing an arbitrarily large service port integer can force the parser to allocate an unbounded bitmap array using an inefficient linear append loop, resulting in severe CPU exhaustion, application hangs, or Out-Of-Memory (OOM) process termination.
CVE-2026-126347 (GHSA-5cjw-f85j-cv6r) is an open redirect vulnerability (CWE-601) in Tornado, an asynchronous Python networking library and web framework. The flaw resides in tornado.web.RedirectHandler when dynamic URL redirection templates incorporate unvalidated user input. Due to a parser differential between RFC 3986 (used by Python's urllib.parse.urlsplit) and the WHATWG URL Standard (used by modern web browsers), inputs containing three or more leading slashes bypass Tornado 6.6.0's validation logic, allowing unauthenticated attackers to redirect users to arbitrary external domains.
A Denial of Service (DoS) vulnerability exists in @sveltejs/adapter-node versions 5.5.7 and 6.0.0-next.0 through 6.0.0. When a SvelteKit server route returns a direct response from a fetch() call with a Content-Type of text/event-stream, the Node.js adapter attempts to mutate the response headers to disable proxy buffering. Because headers returned directly by the WHATWG Fetch API carry an immutable guard, this mutation throws an unhandled TypeError, terminating the underlying Node.js process and denying service to all active and subsequent clients.