Vulnerability lives entirely in client-side JavaScript; attacker data flows into a dangerous DOM sink without passing through the server.
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 | PDF.js glyph-rendering fast path — see CVE-2024-4367 deep-dive below |
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.
new Function() is dangerous precisely because it bypasses the HTML parsing step entirely — no tag injection needed, so it is invisible to sink-hooking scanners that only watch innerHTML or document.write. PDF.js's font-rendering fast path used exactly this sink to compile a PDF's FontMatrix array into executable JavaScript, letting a crafted font definition run arbitrary code the moment a victim opened the file.
Full technical walkthrough — injection payload, affected versions, fix — in the CVE-2024-4367 deep-dive below.
CVE-2024-4367 (published May 14, 2024 on NVD; Mozilla advisories MFSA-2024-21, MFSA-2024-22, MFSA-2024-23) is a DOM-based XSS in Mozilla's PDF.js — the JavaScript PDF renderer built into Firefox and shipped standalone as the pdfjs-dist npm package (~2.7M weekly downloads at disclosure). It scores CVSS 3.1 8.8 HIGH (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H) and is tracked as CWE-754 (Improper Check for Unusual or Exceptional Conditions) — a missing type check, not a classic string-escaping bug.
PDF.js pre-compiles glyph-rendering instructions into JavaScript for performance whenever its isEvalSupported option is enabled — the default at the time. The compiler builds a new Function() body from the font's FontMatrix array, six values the PDF specification defines as numbers. PartialEvaluator.translateFont() reads that array straight from the PDF's font dictionary and joins it into the function source without validating that every entry is actually a number.
Because the PDF format technically allows a FontMatrix entry to be a PDF string, an attacker smuggles one in:
/FontMatrix [1 2 3 4 5 (0\); alert\('CVE-2024-4367'\))]The trailing string closes the legitimate numeric argument list and appends arbitrary JavaScript, which PDF.js then compiles verbatim into the Function body.
FontMatrix.PartialEvaluator.translateFont() extracts the array during font parsing — no type check on individual entries.new Function(...) source string and invokes it — the same Function(string)() sink pattern from the table above, reached through font metadata instead of a URL.resource://pdf.js inside Firefox, or the hosting page's origin for embedded pdfjs-dist), with access to window.PDFViewerApplication.url and any other object that origin can reach.There is no <script> tag and no HTML parsing step involved — the entire chain lives inside a JavaScript Function constructor call, which is why it evaded PDF sanitizers and HTML-focused DOM XSS scanners.
| Affected | pdfjs-dist <= 4.1.392 (all published versions); Firefox < 126; Firefox ESR < 115.11; Thunderbird < 115.11 |
| Fixed | pdfjs-dist 4.2.67 (released April 29, 2024); Firefox 126; Firefox ESR 115.11; Thunderbird 115.11 |
| CVSS 3.1 | 8.8 HIGH — AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H |
| CWE | CWE-754 — Improper Check for Unusual or Exceptional Conditions |
Exploitation requires the victim to open a crafted PDF (UI:R) but no authentication and no prior interaction with the target site — any application rendering untrusted PDFs client-side with a vulnerable pdfjs-dist inherited full JavaScript execution in its own origin.
pdfjs-dist to >= 4.2.67; Firefox to >= 126; Firefox ESR to >= 115.11; Thunderbird to >= 115.11. The patch removes the unsanitized Function() compilation path.// PDF.js viewer options — disables the vulnerable glyph-compilation fast path
const loadingTask = pdfjsLib.getDocument({
url: pdfUrl,
isEvalSupported: false,
});unsafe-eval in script-src blocks the Function constructor outright, closing this entire sink class even against undiscovered variants. It's the same control that helps against the sanitizer-bypass techniques covered in mXSS — Mutation XSS.CVE-2024-4367 is one of the incidents behind the broader shift toward DOM-based attack surface — see BreachVex's XSS Trends 2025–2026 research for the current landscape.
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.