GitHub Advisoryadvisory · npm

GHSA-h8r8-wccr-v5f2 DOMPurify is vulnerable to mutation-XSS via Re-Contextualization

MEDIUM Published 2026-03-27 Modified 2026-07-23 Official page ↗
Aliases
CVE-2026-65914
Ecosystem
npm
Packages
dompurify
Severity
Moderate
CVSS vector
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N
Weaknesses
CWE-79
Reviewed
2026-03-27

Affected packages

• npm dompurify: < 3.3.2 → fixed in 3.3.2

Description

Description A mutation-XSS (mXSS) condition was confirmed when sanitized HTML is reinserted into a new parsing context using `innerHTML` and special wrappers. The vulnerable wrappers confirmed in browser behavior are `script`, `xmp`, `iframe`, `noembed`, `noframes`, and `noscript`. The payload remains seemingly benign after `DOMPurify.sanitize()`, but mutates during the second parse into executable markup with an event handler, enabling JavaScript execution in the client (`alert(1)` in the PoC). Vulnerability The root cause is context switching after sanitization: sanitized output is treated as trusted and concatenated into a wrapper string (for example, ` ... ` or other special wrappers) before being reparsed by the browser. In this flow, attacker-controlled text inside an attribute (for example `` or equivalent closing sequences for each wrapper) closes the special parsing context early and reintroduces attacker markup (``) outside the original attribute context. DOMPurify sanitizes the original parse tree, but the application performs a second parse in a different context, reactivating dangerous tokens (classic mXSS pattern). PoC 1. Start the PoC app: ```bash npm install npm start ``` 2. Open `http://localhost:3001`. 3. Set `Wrapper en sink` to `xmp`. 4. Use payload: ```html "> ``` 5. Click `Sanitize + Render`. 6. Observe: • `Sanitized response` still contains the `` sequence inside `alt`. • The sink reparses to include ``. • `alert('expoc')` is triggered. 7. Files: • index.html ```html expoc - DOMPurify SSR PoC :root { --bg: #f7f8fb; --panel: #ffffff; --line: #d8dce6; --text: #0f172a; --muted: #475569; --accent: #0ea5e9; } • { box-sizing: border-box; } body { margin: 0; font-family: "SF Mono", Menlo, Consolas, monospace; color: var(--text); background: radial-gradient(circle at 10% 0%, #e0f2fe 0%, var(--bg) 60%); } main { max-width: 980px; margin: 28px auto; padding: 0 16px 20px; } h1 { margin: 0 0 10px; font-size: 1.45rem; } p { margin: 0; color: var(--muted); } .grid { display: grid; gap: 14px; margin-top: 16px; } .card { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 14px; } label { display: block; margin-bottom: 7px; font-size: 0.85rem; color: var(--muted); } textarea, input, select, button { width: 100%; border: 1px solid var(--line); border-radius: 8px; padding: 9px 10px; font: inherit; background: #fff; } textarea { min-height: 110px; resize: vertical; } .row { display: grid; grid-template-columns: 1fr 230px; gap: 12px; } button { cursor: pointer; background: var(--accent); color: #fff; border-color: #0284c7; } #sink { min-height: 90px; border: 1px dashed #94a3b8; border-radius: 8px; padding: 10px; background: #f8fafc; } pre { margin: 0; white-space: pre-wrap; word-break: break-word; } .note { margin-top: 8px; font-size: 0.85rem; } .status-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 8px; margin-top: 10px; } .status-item { border: 1px solid var(--line); border-radius: 8px; padding: 8px 10px; font-size: 0.85rem; background: #fff; } .status-item.vuln { border-color: #ef4444; background: #fef2f2; } .status-item.safe { border-color: #22c55e; background: #f0fdf4; } @media (max-width: 760px) { .row { grid-template-columns: 1fr; } } expoc - DOMPurify Server-Side PoC Flujo: input -> POST /sanitize (Node + jsdom + DOMPurify) -> render vulnerable con innerHTML. Payload "> Wrapper en sink div textarea title style script xmp iframe noembed noframes noscript Sanitize + Render Se usa render vulnerable: sink.innerHTML = '&lt;wrapper&gt;' + sanitized + '&lt;/wrapper&gt;'. script (vulnerable) xmp (vulnerable) iframe (vulnerable) noembed (vulnerable) noframes (vulnerable) noscript (vulnerable) div (no vulnerable) textarea (no vulnerable) title (no vulnerable) style (no vulnerable) Sanitized response (empty) Sink const payload = document.getElementById('payload'); const wrapper = document.getElementById('wrapper'); const run = document.getElementById('run'); const sanitizedNode = document.getElementById('sanitized'); const sink = document.getElementById('sink'); run.addEventListener('click', async () => { const response = await fetch('/sanitize', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ input: payload.value }) }); const data = await response.json(); const sanitized = data.sanitized || ''; const w = wrapper.value; sanitizedNode.textContent = sanitized; sink.innerHTML = '' + sanitized + ''; }); ``` • server.js ```js const express = require('express'); const path = require('path'); const { JSDOM } = require('jsdom'); const createDOMPurify = require('dompurify'); co…

Related

Alias · CVE (1)

  • CVE-2026-65914 DOMPurify before 3.3.2 contains a mutation-XSS vulnerability when sanitized HTML is reinserted into special parsing contexts using… vulnerability

Weakness · CWE (1)

  • CWE-79 Improper Neutralization of Input During Web Page Generation weakness

References