DOM-based XSS
Theory
Why This Matters
DOM-based XSS is architecturally distinct from server-side XSS variants because the vulnerability exists entirely in client-side JavaScript — the server never sees the malicious payload, making server-side scanning tools and WAFs blind to it. As single-page applications (SPAs) built with React, Angular, and Vue have become the dominant web architecture, the proportion of XSS vulnerabilities that are DOM-based has grown substantially. Researchers at Google Project Zero and PortSwigger have published extensive work on DOM XSS taint analysis, and Burp Suite's DOM Invader tool was specifically developed to address the gap in dynamic analysis tooling for this class.
Core Concept
DOM-based XSS occurs when JavaScript on a page reads data from an attacker-controllable source and writes it to a dangerous sink without sanitisation. Unlike reflected and stored XSS, the server is not involved in the vulnerability — the injection and execution happen entirely in the browser's DOM manipulation.
Sources are JavaScript properties that contain attacker-controllable data:
- location.hash — the fragment identifier after # (never sent to server)
- document.URL / location.href — the full page URL
- location.search — the query string
- document.referrer — the Referer header value
- URLSearchParams.get() — parsed query parameters
- window.name — persistent across page navigations
- postMessage data — inter-frame messaging
Sinks are JavaScript functions or properties that execute or parse input as HTML/script:
- element.innerHTML = ... — parses as HTML, executes scripts
- document.write(...) — writes raw HTML to the document
- eval(...) — executes a string as JavaScript
- setTimeout(string, ...) / setInterval(string, ...) — execute string as script
- element.src = ... / element.href = ... — may accept javascript: URLs
- location.href = ... — javascript: navigation
The violated invariant is that attacker-controlled data flows from a source to a sink without sanitisation. No server response is needed — the payload in location.hash is processed entirely by the client-side script.
dangerouslySetInnerHTML in React is a DOM XSS sink when user-controlled data is passed to it: <div dangerouslySetInnerHTML={{__html: userContent}} /> — the dangerously prefix is a deliberate developer warning that is frequently ignored.
Taint analysis tracks the flow of attacker-controlled data from source to sink. Manual taint analysis involves reading JavaScript source code; automated taint analysis uses tools like DOM Invader, which instruments the browser to track data flows dynamically.
Technical Deep-Dive
// Vulnerable pattern 1: location.hash → innerHTML
// Page URL: https://target.example.com/page#<img src=x onerror=alert(1)>
document.getElementById('content').innerHTML = location.hash.slice(1);
// hash.slice(1) removes the leading #
// Browser parses innerHTML value as HTML → executes onerror
// Vulnerable pattern 2: location.search → document.write
// URL: https://target.example.com/?name=<script>alert(1)</script>
var name = new URLSearchParams(location.search).get('name');
document.write('<h1>Hello, ' + name + '</h1>');
// document.write parses its argument as HTML
// Vulnerable pattern 3: eval with query parameter
// URL: https://target.example.com/?callback=alert(1)
var cb = new URLSearchParams(location.search).get('callback');
eval(cb); // direct code execution
// Vulnerable pattern 4: setTimeout with string
var action = location.hash.slice(1);
setTimeout(action, 1000); // action is executed as JS after 1 second
// React dangerous pattern:
function Comment({ userInput }) {
// VULNERABLE: user input rendered as raw HTML
return <div dangerouslySetInnerHTML={{ __html: userInput }} />;
}
// Secure: use {userInput} which React escapes automatically:
function CommentSafe({ userInput }) {
return <div>{userInput}</div>;
}
// DOM Invader (Burp Suite) usage:
// 1. Open Burp Browser (Chromium embedded in Burp)
// 2. Navigate to target
// 3. Open DevTools → DOM Invader tab
// 4. Enable DOM Invader → Enable canary injection
// 5. DOM Invader automatically injects a unique canary string into all sources
// 6. Monitors all sinks for canary presence
// 7. Highlights tainted sink calls with source→sink trace
// Manual source-to-sink tracing in browser DevTools:
// 1. Sources tab → search all JS files for: innerHTML, document.write, eval
// 2. Set breakpoints on each sink call
// 3. Manipulate URL hash/params to introduce canary
// 4. Observe breakpoints to trace data flow
# Static analysis with grep for DOM sinks in JavaScript files:
grep -rn --include="*.js"
-E 'innerHTML|document.write|eval(|setTimeout(.*string|setInterval|dangerouslySetInnerHTML'
./static/
# Also search for sources:
grep -rn --include="*.js"
-E 'location.hash|location.search|document.URL|document.referrer|window.name'
./static/
# Combine: find files where sources and sinks appear in close proximity
Security Assessment Methodology
- Enumerate JavaScript files — Download and review all JS files served by the application. Use Burp's JS analysis or wget to mirror JS resources.
- Grep for sources and sinks — Search for
location.hash,location.search,document.URL,document.referreras sources; andinnerHTML,document.write,eval,setTimeout(string form),dangerouslySetInnerHTMLas sinks. - Trace source-to-sink flows — For each sink, trace whether any attacker-controllable source reaches it without sanitisation. Use browser DevTools breakpoints to verify dynamically.
- Use DOM Invader — Open the target in Burp's embedded browser with DOM Invader enabled. Navigate the application fully; review DOM Invader's highlighted sink calls and their source traces.
- Craft context-appropriate payloads — For
innerHTMLsinks:<img src=x onerror=alert(1)>; forevalsinks:alert(1); forlocation.href:javascript:alert(1). - Test hash-based sources — Because
location.hashis not sent to the server, these payloads bypass WAFs and server-side filters entirely. Always test hash-delivered payloads. - Verify in browser — Confirm payload execution in a real browser (Chrome and Firefox); note any DOM Purify or sanitisation library in use that may block exploitation.
Defensive Countermeasure — Avoid dangerous sinks: replace
element.innerHTML = datawithelement.textContent = datawhen the data does not need to be rendered as HTML. For React, use JSX interpolation ({value}) instead ofdangerouslySetInnerHTML. When HTML rendering is required, sanitise with DOMPurify (DOMPurify.sanitize(userContent)) immediately before insertion into a dangerous sink. Implement a Content Security Policy withscript-src 'self'to block inline script execution as a defence-in-depth layer. Regularly audit JavaScript files for new sink introductions using automated SAST tools (Semgrep, ESLint with security plugins).
Common Assessment Errors
- Relying solely on server-side scanning — DAST tools that analyse HTTP responses miss DOM XSS because the payload is in
location.hash(never sent to server) and the vulnerability is in client-side JS. Browser-based tools (DOM Invader) are required. - Not checking
location.hash— This source is never transmitted to the server, making it the most WAF-resistant XSS delivery mechanism. It is systematically omitted in assessments that focus only on server-reflected parameters. - Missing
postMessagehandlers —window.addEventListener('message', ...)handlers that passevent.datato innerHTML are increasingly common in SPA architectures and are frequently missed. - Treating React as automatically safe — React auto-escapes JSX interpolations but
dangerouslySetInnerHTMLand direct DOM manipulation viauseRefbypass React's protection. Never assume a React application is XSS-free without examining these patterns. - Not tracing sanitisation paths — A sink with
DOMPurify.sanitize()wrapping may still be vulnerable if the sanitised output is passed to a second, unguarded sink. Trace the full data flow. - Stopping at source identification — Finding that
location.hashfeedsinnerHTMLis not a confirmed vulnerability until you demonstrate a working payload in a browser.
NICE Framework Alignment
| Code | Knowledge/Skill/Task Statement | How This Card Develops It |
|---|---|---|
| K0009 | Knowledge of application vulnerabilities | Develops precise source-sink taint model understanding for DOM XSS across multiple JavaScript patterns |
| K0070 | Knowledge of system and application security threats and vulnerabilities | Covers modern SPA frameworks (React), hash-based sources, and postMessage attack surface |
| S0001 | Skill in conducting vulnerability scans | Trains DOM Invader usage and grep-based static JS analysis methodology |
| S0044 | Skill in mimicking threat behaviors | Builds adversarial skill in browser-based payload delivery via hash fragments |
| T0028 | Test system security controls | Covers dangerous sink usage review and DOMPurify integration assessment |
| T0028 | Test system security controls | Covers client-side JS code review for source-sink taint flows |
Further Reading
- DOM-Based Cross-Site Scripting — PortSwigger Web Security Academy
- DOM Invader Documentation — PortSwigger Burp Suite Pro
- Taint Tracking for JavaScript — Google Project Zero research publications
Challenge Lab
Reinforce your learning with a hands-on generated challenge based on this card's competency.