DOMPurify CVE-2026-41239
MEDIUMSeverity by source
AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:N
Primary rating from GitHub Advisory.
CVSS VectorGitHub Advisory
CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:N
Lifecycle Timeline
4Blast Radius
ecosystem impact- 6 npm packages depend on dompurify (4 direct, 2 indirect)
Ecosystem-wide dependent count for version 1.0.10.
DescriptionGitHub Advisory
Summary
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:
- Per-node - each text node is checked during the tree walk (
purify.ts:1179-1191):
// 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;
}- Final string scrub - after serialization, the full HTML string is scrubbed again (
purify.ts:1679-1683). 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):
// 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:
- Parser creates:
TEXT("{")→<foo>→TEXT("{payload}")→<foo>→TEXT("}")- no single node contains{{, so pass #1 misses it <foo>is not allowed, so DOMPurify removes it but keeps surrounding text- The three text nodes are now adjacent -
.outerHTMLreads them as{{payload}}, which Vue 2 compiles and executes
Reproduce
Open the following html in any browser and alert(1) pops up.
<!DOCTYPE html>
<html>
<body>
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.3.3/dist/purify.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.min.js"></script>
<script>
var dirty = '<div id="app">{<foo></foo>{constructor.constructor("alert(1)")()}<foo></foo>}</div>';
var dom = DOMPurify.sanitize(dirty, { SAFE_FOR_TEMPLATES: true, RETURN_DOM: true });
document.body.appendChild(dom.firstChild);
new Vue({ el: '#app' });
</script>
</body>
</html>Impact
Any application that sanitizes attacker-controlled HTML with SAFE_FOR_TEMPLATES: true and RETURN_DOM: true (or RETURN_DOM_FRAGMENT: true), then mounts the result into a template-evaluating framework, is vulnerable to XSS.
Recommendations
Fix
normalize() merges the split text nodes, then the same regex from the string path catches the expression. Placed before the fragment logic, this fixes both RETURN_DOM and RETURN_DOM_FRAGMENT.
if (RETURN_DOM) {
+ if (SAFE_FOR_TEMPLATES) {
+ body.normalize();
+ let html = body.innerHTML;
+ arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], (expr: RegExp) => {
+ html = stringReplace(html, expr, ' ');
+ });
+ body.innerHTML = html;
+ }
+
if (RETURN_DOM_FRAGMENT) {
returnNode = createDocumentFragment.call(body.ownerDocument);AnalysisAI
Cross-site scripting (XSS) in DOMPurify when using SAFE_FOR_TEMPLATES with RETURN_DOM or RETURN_DOM_FRAGMENT modes allows remote attackers to execute arbitrary JavaScript by crafting malformed HTML that reassembles into template expressions after DOM normalization. The vulnerability affects DOMPurify from v1.0.10 through at least v3.3.3, exploitable when sanitized output is mounted into template-evaluating frameworks like Vue 2. A proof-of-concept demonstrates reliable exploitation with alert(1) execution.
Technical ContextAI
DOMPurify implements SAFE_FOR_TEMPLATES mode to strip template expressions (Mustache {{...}}, ERB <%...%>, and template literals ${...}) from HTML in two passes: first during DOM tree traversal (per-node), then via regex on the serialized HTML string. However, when RETURN_DOM or RETURN_DOM_FRAGMENT is specified, the function returns early before executing the second pass. An attacker can exploit this by splitting a template expression across multiple text nodes separated by disallowed elements (e.g., {<foo></foo>{payload}<foo></foo>}). During tree traversal, no single text node contains the complete {{...}} pattern, so pass #1 misses it. After DOMPurify removes the disallowed <foo> tags, adjacent text nodes are merged during DOM serialization, reconstructing the full template expression. When this malformed DOM is serialized to HTML and mounted in Vue 2 (or similar frameworks), the template expression is evaluated, executing attacker code. The root cause is incomplete sanitization of template expressions when the DOM path is taken (CWE-79: Improper Neutralization of Input During Web Page Generation).
RemediationAI
Vendor-released patch: DOMPurify v3.4.0 and later. Upgrade immediately via npm update dompurify or update package.json to dompurify@>=3.4.0. The fix adds a normalize() call and regex scrubbing in the RETURN_DOM path before returning the DOM fragment, ensuring split template expressions are merged and re-checked. See patch details and release notes at https://github.com/cure53/DOMPurify/releases/tag/3.4.0. For applications that cannot immediately upgrade, workarounds include: (1) Avoid RETURN_DOM and RETURN_DOM_FRAGMENT modes-use string return (default) instead, which executes both sanitization passes; (2) If DOM return is required, disable SAFE_FOR_TEMPLATES if template framework is not used, or sanitize output a second time by converting to string, re-sanitizing, and re-parsing; (3) Apply Content Security Policy (CSP) with script-src restrictions to limit XSS impact, though this is a defense-in-depth measure, not a complete mitigation. Trade-off: Workaround (1) may require refactoring DOM handling; workaround (2) adds performance overhead.
FortiOS and FortiProxy contain an authentication bypass via the Node.js websocket module allowing unauthenticated remote
Eval injection vulnerability in the internals.batch function in lib/batch.js in the bassmaster plugin before 1.5.2 for t
Flowise version 3.0.5 contains a remote code execution vulnerability in the CustomMCP node. The mcpServerConfig paramete
Node.js 8.5.0 before 8.6.0 allows remote attackers to access unintended files, because a change to ".." handling was inc
An issue was discovered in the node-serialize package 0.0.4 for Node.js. Rated critical severity (CVSS 9.8), this vulner
Directory traversal vulnerability in the st module before 0.2.5 for Node.js allows remote attackers to read arbitrary fi
Multiple SQL injection vulnerabilities in the Manage Accounts page in the AccountManagement.asmx service in the Solarwin
The JS-YAML module before 2.0.5 for Node.js parses input without properly considering the unsafe !!js/function tag, whic
Directory traversal vulnerability in lib/app/index.js in Geddy before 13.0.8 for Node.js allows remote attackers to read
Credential-harvesting malware compromised 84 versions of 42 TanStack npm packages on 2026-05-11 via chained GitHub Actio
Eval injection vulnerability in index.js in the syntax-error package before 1.1.1 for Node.js 0.10.x, as used in IBM Rat
The HTTP server in Node.js 0.10.x before 0.10.21 and 0.8.x before 0.8.26 allows remote attackers to cause a denial of se
Same weakness CWE-79 – Cross-site Scripting (XSS)
View allVendor StatusVendor
Share
External POC / Exploit Code
Leaving vuln.today
GHSA-crv5-9vww-q3g8