Nivel 2 · 25 min
XSS: Stored, Reflected y DOM-based
Cross-Site Scripting (XSS) es lo que pasa cuando texto controlado por el atacante llega al browser como HTML, JS, u otro contexto activo. El código del atacante corre en la sesión de la víctima — same-origin — así que puede leer cookies (si no son HttpOnly), llamar a cualquier API que la víctima pueda, y exfiltrar el resultado. La defensa es output encoding contextual más una Content-Security-Policy que limite el daño aunque falle el encoding.
Tres Variantes que Vas a Ver
Stored XSS — el payload se persiste (un comentario, un campo de perfil, un post en Markdown) y corre cada vez que otro usuario lo ve. Mayor impacto: un solo payload, todos los visitantes comprometidos. Reflected XSS — el payload va en la URL o en el envío de un formulario y se renderea de vuelta sin persistir: search?q='<'script'>'... es el ejemplo de manual. El atacante engaña a la víctima para que clickee el link. DOM-based XSS — el bug está en JavaScript del lado cliente: document.body.innerHTML = location.hash.slice(1). El servidor nunca ve el payload; la defensa tiene que pasar en el browser. Una variante particularmente desagradable es stored XSS en renderers de Markdown — muchas libs de Markdown permiten HTML inline, y una configuración insegura que no quita '<'script'>' o hrefs javascript: hace que cada comentario o página de wiki sea un vector.
Output Encoding Contextual
No existe una sola función 'escape' que ande en todos lados. El encoding depende de dónde se interpola el valor. HTML body — encodeá < > & como < > &. Atributo HTML — encodeá también '"' como " y preferí atributos siempre con comillas. String literal de JavaScript — las reglas son distintas: < debe ser \u003c (así </script> adentro de un string JS no puede romperse afuera), más comilla y backslash. Componente de URL — encodeURIComponent. CSS — solo permití constantes en allowlist. Los frameworks modernos hacen casi todo esto por vos (los nodos de texto JSX en React están encodeados; el {'{value}'} de Angular está encodeado), pero las escape hatches son minas: dangerouslySetInnerHTML en React, [innerHTML] en Angular, v-html en Vue. Templates que arman URLs dinámicamente (href={url}) necesitan validación de URL: rechazá esquemas javascript: y data:.
CSP y Flags de Cookie como Defensa en Profundidad
Hasta el mejor encoding va a fallar tarde o temprano. Content-Security-Policy es un header que le dice al browser qué fuentes de scripts/estilos/imágenes están permitidas. Una política estricta: Content-Security-Policy: default-src ''self''; script-src ''self'' ''nonce-AbCdEf123''; object-src ''none''; base-uri ''self''. El nonce se regenera por respuesta y se agrega a cada '<'script'>' que el servidor emite — los '<'script'>' sin el nonce que matchea no se ejecutan, aunque hayan sido inyectados. Evitá ''unsafe-inline'' y ''unsafe-eval''. Las cookies siempre deben ser Secure (solo HTTPS), HttpOnly (sin acceso desde JS — mata el camino de robo de cookies del XSS), y SameSite=Lax o Strict (también ayuda con CSRF). HSTS (Strict-Transport-Security: max-age=31536000; includeSubDomains; preload) previene downgrade de protocolo. Ninguna de estas reemplaza no tener XSS — limitan el radio de impacto cuando lo tenés.
Code example
// VULNERABLE — innerHTML con input de usuario
document.getElementById("out").innerHTML = userInput;
// CORREGIDO — textContent (los browsers escapean automáticamente)
document.getElementById("out").textContent = userInput;
// VULNERABLE — JSP sin escape
<div>Bienvenido <%= request.getParameter("name") %>'</div>
// CORREGIDO — JSTL <c:out> escapea por defecto
<div>Bienvenido <c:out value="${param.name}" />'</div>
// Header CSP (nonce regenerado por respuesta)
response.setHeader("Content-Security-Policy",
"default-src 'self'; " +
"script-src 'self' 'nonce-" + nonce + "'; " +
"object-src 'none'; base-uri 'self'");
// <script nonce="${nonce}">...</script> // se ejecuta
// <script>evil()</script> // bloqueado