Vulnerability lives entirely in client-side JavaScript; attacker data flows into a dangerous DOM sink without passing through the server.
TL;DR
location.hash, postMessage) to a sink (innerHTML, eval)new Function() sink in font renderer executed arbitrary JSDOM-based XSS (CWE-79, Type-0 XSS in the OWASP taxonomy) is a variant where the vulnerability exists entirely within client-side JavaScript. The server's response is legitimate — no malicious data passes through the HTTP layer. Instead, client-side code reads attacker-controlled data from a DOM source and writes it to a dangerous sink without sanitization. The script executes in the victim's browser under the application's origin.
This makes DOM XSS fundamentally different from reflected and stored variants: server-side WAFs see no attack traffic, and any DAST tool that only examines HTTP responses will not detect it. Detection requires dynamic JavaScript analysis — a headless browser instrumenting the actual execution, tracing data from source to sink.
The source → sink data flow is the core model of DOM XSS analysis.
Sources — attacker-controllable inputs that JavaScript can read:
| Source | Notes |
|---|---|
location.search | URL query string — most common |
location.hash | Fragment identifier — never sent to server, blind to WAFs |
document.referrer | Controlled by attacker via navigation |
window.name | Persists across navigations; bypasses same-origin |
postMessage data | Cross-origin; requires no event.origin validation |
localStorage / sessionStorage | Previously tainted data |
document.cookie | Cookie injection via CRLF or subdomain |
| WebSocket messages | Real-time data streams |
Sinks — dangerous DOM write operations that can produce script execution:
| Sink | Danger | Notes |
|---|---|---|
element.innerHTML | Critical | Executes event handlers and <script> |
document.write() | Critical | Breaks parser context |
eval() | Critical | Direct code execution |
setTimeout(string, ...) | Critical | String argument evaluated as JS |
setInterval(string, ...) | Critical | Same |
Function(string)() | Critical | Used in PDF.js (CVE-2024-4367) |
element.insertAdjacentHTML() | Critical | Equivalent to innerHTML |
location.href = "javascript:..." | High | Redirect to JS URI |
element.setAttribute("onerror", ...) | High | Event handler injection |
element.src / element.href | Medium | data: / javascript: URI |
The classic DOM XSS pattern reads from location.search and writes to innerHTML:
// VULNERABLE — source to sink without sanitization
const params = new URLSearchParams(location.search);
const name = params.get('name');
document.getElementById('greeting').innerHTML = 'Hello, ' + name;
// Payload: ?name=<img src=x onerror=alert(document.domain)>
// The browser inserts the raw HTML, executes the onerror handlerThe server returns a normal 200 response. The entire vulnerability exists in two lines of client JavaScript.
postMessage variant — exploitable from any cross-origin page:
// VULNERABLE — no origin validation
window.addEventListener('message', function(e) {
// e.origin not checked — any origin can send
document.getElementById('content').innerHTML = e.data; // sink
});// Attacker page (any origin):
const target = window.open('https://victim.com/app');
setTimeout(() => {
target.postMessage('<img src=x onerror=alert(document.domain)>', '*');
}, 1000);CVE-2024-49038 (Microsoft Copilot Studio, CVSS 9.3) exploited a postMessage/DOM injection pattern in a cloud AI service, allowing cross-tenant privilege escalation.
Prototype pollution sets arbitrary properties on Object.prototype via crafted query parameters. When application code or a library reads an undefined property from any object, it falls back to the polluted prototype.
// Step 1: Prototype pollution via URL
// ?__proto__[transport_url]=data:,alert(document.domain);
// Sets: Object.prototype.transport_url = "data:,alert(1)"
// Step 2: jQuery gadget reads the property
$.ajax({ url: '/api', jsonp: callback });
// jQuery internally reads options.transport_url — undefined on options object
// Falls back to Object.prototype.transport_url → attacker's payload executesCVE-2026-41238 demonstrates that DOMPurify itself is a prototype pollution gadget: polluting Object.prototype.tagNameCheck with a permissive regex causes DOMPurify to pass arbitrary elements through sanitization, converting a prototype pollution vulnerability anywhere in the application into a full XSS bypass.
Detection tools: Burp Suite DOM Invader (built-in prototype pollution scanner), PPScan Chrome Extension, and the client-side-prototype-pollution GitHub repository (maintained gadget list by BlackFan).
DOM clobbering uses named HTML elements — injected via HTML injection in contexts where <script> is blocked — to shadow JavaScript global variables.
<!-- Single-level clobber: window.x becomes an HTMLElement -->
<a id="x" href="https://attacker.com/malicious.js"></a>
<!-- Two-level clobber: window.config.cdn becomes a string via anchor href -->
<a id="config"><a id="config" name="cdn" href="https://attacker.com">When application code executes var url = window.config.cdn || '/assets/', the clobbered config.cdn value is https://attacker.com — attacker-controlled.
CVE-2024-7524 — Firefox CSP strict-dynamic bypass: The Facebook SDK ETP shim reads document.currentScript.src without validating the tagName. Injecting <img name="currentScript" src="data:,alert(document.domain)"> causes the shim to load the data: URI as a trusted child script under strict-dynamic, bypassing the CSP entirely.
DOM clobbering escalates HTML injection — where <script> is blocked — into full script execution. An application that sanitizes <script> but allows id and name attributes on <a> and <form> elements remains vulnerable to DOM clobbering XSS chains.
PDF.js's font rendering pipeline compiles glyph instructions into new Function() bodies. A controllable FontMatrix array in PDF metadata — inserted raw into the function body without type validation — closes the legitimate call and appends arbitrary JavaScript.
# Malicious PDF font definition
/FontMatrix [1 2 3 4 5 (0\); alert\('CVE-2024-4367'\))]CVSS 8.8 HIGH. Affected all Firefox versions < 126 and all web/Electron applications embedding pdfjs-dist (~2.7M weekly npm downloads). The new Function() sink is dangerous precisely because it bypasses the HTML parsing step entirely — no tag injection needed.
location.search, location.hash, document.referrer, window.name, addEventListener('message', ...)postMessage with XSS payload from an attacker-controlled origin// Quick manual test — paste in browser console
// Tests if location.hash reaches innerHTML
const hash = decodeURIComponent(location.hash.slice(1));
// Check all innerHTML assignments manually in browser DevTools Sources tabBreachVex detects DOM XSS using instrumented headless-browser sessions that hook all dangerous sinks at page load, trace canary arrival at each sink with full stack traces, and confirm execution by catching the resulting dialog.
Trusted Types (enforced on YouTube, Microsoft 365, and Stripe) prevents DOM sinks from accepting raw strings entirely:
// CSP header to enforce:
// Content-Security-Policy: require-trusted-types-for 'script'
// Safe policy — DOMPurify sanitizes before assignment
const policy = trustedTypes.createPolicy('default', {
createHTML: (input) => DOMPurify.sanitize(input),
});
element.innerHTML = policy.createHTML(userInput); // OK — goes through sanitizer
element.innerHTML = userInput; // TypeError in enforcement modeThis prevents DOM XSS even when a source-to-sink flow exists — the browser throws before the sink executes.
Replace dangerous sinks with safe equivalents:
// VULNERABLE: innerHTML with user data
element.innerHTML = userInput;
// SAFE: textContent for plain text (never renders HTML)
element.textContent = userInput;
// SAFE: structured DOM construction (no HTML parsing)
const p = document.createElement('p');
p.textContent = userInput;
container.appendChild(p);
// SAFE: DOMPurify + Trusted Types when HTML is needed
element.innerHTML = trustedTypesPolicy.createHTML(DOMPurify.sanitize(userInput));// VULNERABLE — no origin check
window.addEventListener('message', function(e) {
element.innerHTML = e.data;
});
// SAFE — strict allowlist of expected origins
const ALLOWED_ORIGINS = new Set(['https://app.example.com', 'https://checkout.example.com']);
window.addEventListener('message', function(e) {
if (!ALLOWED_ORIGINS.has(e.origin)) return; // reject unknown origins
element.textContent = e.data; // use safe sink
});// Freeze Object.prototype to prevent pollution
Object.freeze(Object.prototype);
// Or: use Object.create(null) for config objects (no prototype chain)
const config = Object.create(null);
config.timeout = 5000;
// Object.prototype pollution cannot reach config propertiesDOM-based XSS occurs entirely in client-side JavaScript — no malicious data reaches the server, and the server's HTTP response is clean. Reflected XSS requires the server to echo the payload in its response. DOM XSS is invisible to server-side WAFs and most DAST tools that examine HTTP responses.
Sources are attacker-controllable inputs read by JavaScript: location.search, location.hash, document.referrer, window.name, postMessage event data, localStorage, sessionStorage, document.cookie, WebSocket messages, and IndexedDB.
Sinks are dangerous DOM write operations: innerHTML, outerHTML, document.write(), eval(), setTimeout(string), setInterval(string), Function(string)(), insertAdjacentHTML(), location.href with javascript: URI, element.src/href for data: URIs, and jQuery's .html() method.
Prototype pollution allows an attacker to set arbitrary properties on Object.prototype via query parameters like ?__proto__[key]=val. When application code or a library reads an undefined property from an object, it falls back to the polluted prototype, which the attacker has set to an XSS payload. CVE-2026-41238 shows DOMPurify itself is a gadget target.
DOM clobbering overwrites JavaScript global variables with named HTML elements. An HTML injection of an anchor element with id 'x' creates window.x as an HTMLElement, shadowing any JavaScript variable named x. This can escalate HTML injection (where script tags are blocked) into full XSS by corrupting a variable used as a script URL or innerHTML source.
DOM XSS requires dynamic analysis: Burp Suite DOM Invader (instruments Chrome to trace source-to-sink data flow), headless browser automation with taint tracking (Playwright), or CodeQL semantic analysis. Server-side scanners examining HTTP responses cannot detect it.
Yes — Trusted Types enforces that all DOM sink assignments go through a developer-defined policy. When require-trusted-types-for 'script' is in the CSP, assigning a raw string to innerHTML throws a TypeError. This eliminates DOM XSS at the platform level for compliant applications. Chrome/Edge enforce it; Firefox support is in progress.
CVE-2024-4367 exploited PDF.js's font rendering pipeline, which compiled glyph instructions into new Function() bodies. A controllable FontMatrix array in PDF metadata injected arbitrary JavaScript that executed on the embedding domain. With ~2.7M weekly npm downloads, this affected all web and Electron apps embedding PDF.js.