CVE-2026-41239
Published:April 23, 2026
Updated:April 25, 2026
Summary | Field | Value | |:------|:------| | Severity | Medium | | Affected | DOMPurify "main" at ""883ac15"" (https://github.com/cure53/DOMPurify/tree/883ac15d47f907cb1a3b5a152fe90c4d8c10f9e6), introduced in v1.0.10 (""7fc196db"" (https://github.com/cure53/DOMPurify/commit/7fc196db0b42a0c360262dba0cc39c9c91bfe1ec)) | "SAFE_FOR_TEMPLATES" strips "{{...}}" expressions from untrusted HTML. This works in string mode but not with "RETURN_DOM" or "RETURN_DOM_FRAGMENT", allowing XSS via template-evaluating frameworks like Vue 2. Technical Details DOMPurify strips template expressions in two passes: 1. Per-node — each text node is checked during the tree walk (""purify.ts:1179-1191"" (https://github.com/cure53/DOMPurify/blob/883ac15d47f907cb1a3b5a152fe90c4d8c10f9e6/src/purify.ts#L1179-L1191)): // pass #1: runs on every text node during tree walk if (SAFE_FOR_TEMPLATES && currentNode.nodeType === NODE_TYPE.text) { content = currentNode.textContent; content = content.replace(MUSTACHE_EXPR, ' '); // {{...}} -> ' ' content = content.replace(ERB_EXPR, ' '); // <%...%> -> ' ' content = content.replace(TMPLIT_EXPR, ' '); // ${... -> ' ' currentNode.textContent = content; } 2. Final string scrub — after serialization, the full HTML string is scrubbed again (""purify.ts:1679-1683"" (https://github.com/cure53/DOMPurify/blob/883ac15d47f907cb1a3b5a152fe90c4d8c10f9e6/src/purify.ts#L1679-L1683)). This is the safety net that catches expressions that only form after the DOM settles. The "RETURN_DOM" path returns before pass #2 ever runs (""purify.ts:1637-1661"" (https://github.com/cure53/DOMPurify/blob/883ac15d47f907cb1a3b5a152fe90c4d8c10f9e6/src/purify.ts#L1637-L1661)): // purify.ts (simplified) if (RETURN_DOM) { // ... build returnNode ... return returnNode; // <-- exits here, pass #2 never runs } // pass #2: only reached by string-mode callers if (SAFE_FOR_TEMPLATES) { serializedHTML = serializedHTML.replace(MUSTACHE_EXPR, ' '); } return serializedHTML; The payload "{<foo></foo>{constructor.constructor('alert(1)')()}<foo></foo>}" exploits this: 3. Parser creates: "TEXT("{")" → "<foo>" → "TEXT("{payload}")" → "<foo>" → "TEXT("}")" — no single node contains "{{", so pass #1 misses it 4. "<foo>" is not allowed, so DOMPurify removes it but keeps surrounding text 5. The three text nodes are now adjacent — ".outerHTML" reads them as "{{payload}}", which Vue 2 compiles and executes
Affected Packages
dompurify (CDN_JS):
Affected version(s) >=1.0.10 <3.4.0Fix Suggestion:
Update to version 3.4.0dompurify (NPM):
Affected version(s) >=1.0.10 <3.4.0Fix Suggestion:
Update to version 3.4.0Additional Notes
The description of this vulnerability differs from MITRE.
Related Resources (3)
Do you need more information?
Contact UsCVSS v4
Base Score:
7.6
Attack Vector
NETWORK
Attack Complexity
HIGH
Attack Requirements
NONE
Privileges Required
NONE
User Interaction
PASSIVE
Vulnerable System Confidentiality
HIGH
Vulnerable System Integrity
HIGH
Vulnerable System Availability
NONE
Subsequent System Confidentiality
NONE
Subsequent System Integrity
NONE
Subsequent System Availability
NONE
CVSS v3
Base Score:
6.8
Attack Vector
NETWORK
Attack Complexity
HIGH
Privileges Required
NONE
User Interaction
REQUIRED
Scope
UNCHANGED
Confidentiality
HIGH
Integrity
HIGH
Availability
NONE
Weakness Type (CWE)
EPSS
Base Score:
0.05