Skip to main content

DOMPurify CVE-2026-41239

MEDIUM
Cross-site Scripting (XSS) (CWE-79)
2026-04-22 https://github.com/cure53/DOMPurify GHSA-crv5-9vww-q3g8
6.8
CVSS 3.1 · GitHub Advisory
Share

Severity by source

GitHub Advisory PRIMARY
6.8 MEDIUM
AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:N
Red Hat
6.8 MEDIUM
qualitative

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
Attack Vector
Network
Attack Complexity
High
Privileges Required
None
User Interaction
Required
Scope
Unchanged
Confidentiality
High
Integrity
High
Availability
None

Lifecycle Timeline

4
Analysis Generated
Apr 23, 2026 - 07:03 vuln.today
Patch released
Apr 23, 2026 - 02:30 nvd
Patch available
Analysis Generated
Apr 22, 2026 - 18:01 vuln.today
CVE Published
Apr 22, 2026 - 17:32 nvd
MEDIUM 6.8

Blast Radius

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

Ecosystem-wide dependent count for version 1.0.10.

DescriptionGitHub Advisory

Summary

FieldValue
SeverityMedium
AffectedDOMPurify main at 883ac15, introduced in v1.0.10 (7fc196db)

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):
js
// 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;
}
  1. 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):

js
// 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:

  1. Parser creates: TEXT("{")<foo>TEXT("{payload}")<foo>TEXT("}") - no single node contains {{, so pass #1 misses it
  2. <foo> is not allowed, so DOMPurify removes it but keeps surrounding text
  3. The three text nodes are now adjacent - .outerHTML reads them as {{payload}}, which Vue 2 compiles and executes

Reproduce

Open the following html in any browser and alert(1) pops up.

html
<!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.

diff
     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.

CVE-2024-55591 CRITICAL POC
9.8 Jan 14

FortiOS and FortiProxy contain an authentication bypass via the Node.js websocket module allowing unauthenticated remote

CVE-2014-7205 CRITICAL POC
10.0 Oct 08

Eval injection vulnerability in the internals.batch function in lib/batch.js in the bassmaster plugin before 1.5.2 for t

CVE-2025-59528 CRITICAL POC
10.0 Sep 22

Flowise version 3.0.5 contains a remote code execution vulnerability in the CustomMCP node. The mcpServerConfig paramete

CVE-2017-14849 HIGH POC
7.5 Sep 28

Node.js 8.5.0 before 8.6.0 allows remote attackers to access unintended files, because a change to ".." handling was inc

CVE-2017-5941 CRITICAL POC
9.8 Feb 09

An issue was discovered in the node-serialize package 0.0.4 for Node.js. Rated critical severity (CVSS 9.8), this vulner

CVE-2014-3744 HIGH POC
7.5 Oct 23

Directory traversal vulnerability in the st module before 0.2.5 for Node.js allows remote attackers to read arbitrary fi

CVE-2014-9566 HIGH POC
7.5 Mar 10

Multiple SQL injection vulnerabilities in the Manage Accounts page in the AccountManagement.asmx service in the Solarwin

CVE-2013-4660 MEDIUM POC
6.8 Jun 28

The JS-YAML module before 2.0.5 for Node.js parses input without properly considering the unsafe !!js/function tag, whic

CVE-2015-5688 MEDIUM POC
5.0 Sep 04

Directory traversal vulnerability in lib/app/index.js in Geddy before 13.0.8 for Node.js allows remote attackers to read

CVE-2026-45321 CRITICAL POC
9.6 May 12

Credential-harvesting malware compromised 84 versions of 42 TanStack npm packages on 2026-05-11 via chained GitHub Actio

CVE-2014-7192 CRITICAL POC
10.0 Dec 11

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

CVE-2013-4450 MEDIUM POC
5.0 Oct 21

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

Vendor StatusVendor

Share

CVE-2026-41239 vulnerability details – vuln.today

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