Definición
El XSS (Cross-Site Scripting) consiste en conseguir que una página web ejecute JavaScript elegido por el atacante en el navegador de otro usuario.
Y esa es la parte que hay que entender bien: el código se ejecuta con los permisos de la víctima. Puede leer sus cookies, hacer peticiones como si fuera él, modificar lo que ve en pantalla o robarle la sesión. Para el navegador es código legítimo de tu web, porque viene de tu dominio.
Si la inyección SQL ataca tu base de datos, el XSS ataca a tus usuarios usando tu web como vehículo.
Los tres tipos
Almacenado. El más grave. El código malicioso se guarda en tu base de datos —en un comentario, en el nombre de un producto, en un mensaje— y se ejecuta cada vez que alguien carga esa página. Un solo ataque afecta a todos los visitantes.
Reflejado. El código viaja en la dirección y la página lo devuelve en su respuesta. Requiere convencer a la víctima de pulsar un enlace preparado.
Basado en DOM. Ni siquiera pasa por el servidor: es JavaScript del propio cliente que coge algo de la dirección y lo mete en la página sin limpiarlo.
La defensa: escapar la salida
La regla principal es corta: escapa siempre al pintar, nunca confíes en haber limpiado al guardar.
Escapar significa convertir los caracteres especiales del HTML en sus equivalentes seguros: < pasa a <, > pasa a >, las comillas también. Así el navegador los muestra como texto en vez de interpretarlos como etiquetas.
echo htmlspecialchars($comentario, ENT_QUOTES, 'UTF-8');
En el navegador, la diferencia clave:
elemento.textContent = dato; // seguro: siempre texto
elemento.innerHTML = dato; // peligroso: interpreta etiquetas
React, Vue y los frameworks modernos escapan por defecto, y por eso el XSS es mucho menos frecuente en ellos. La puerta que queda abierta es la que se salta esa protección a propósito: en React, dangerouslySetInnerHTML. Se llama así por algo.
Ejemplo práctico
En esta web hay dos sitios donde el riesgo era real y se resolvió de forma distinta.
El primero, el JSON-LD. Los datos estructurados que lee Google se meten dentro de una etiqueta <script>, y su contenido sale de los términos del diccionario, que escribo yo en Markdown. Si algún día un término llevara un < en el sitio equivocado, podría cerrar la etiqueta antes de tiempo y lo que viniera después se interpretaría como HTML.
La solución es la que recomienda la documentación de Next: sustituir cada < por su escape Unicode < antes de insertar el JSON. El navegador lo interpreta igual al leer los datos, y ya no queda ningún < literal con el que cerrar la etiqueta desde el contenido.
El segundo, el correo del CRM de IMDICA. El panel muestra los correos entrantes con su formato original, y un correo es HTML que envía un desconocido. Ahí no basta con escapar, porque entonces se vería el código en bruto en vez del correo.
La solución fue distinta: confinar el HTML del correo dentro de un contenedor propio y reescribir sus estilos para que no puedan afectar al resto del panel. Se muestra el correo tal cual, y su CSS no puede salir de su caja.
Los dos casos comparten la enseñanza: el contenido que no escribes tú se trata como hostil, aunque venga de un cliente conocido.
Capas adicionales
- Política de seguridad de contenido (CSP). Una cabecera que le dice al navegador de dónde puede cargar scripts. Bien configurada, aunque un atacante consiga inyectar, el navegador se niega a ejecutar.
- Cookies
HttpOnly. Así el JavaScript no puede leer la cookie de sesión, que es el objetivo más habitual. - Cookies
SecureySameSite. Contra el robo por red y contra el CSRF. - Sanear el HTML que sí permites. Si tu aplicación deja usar negritas y enlaces, usa una librería específica y una lista blanca de etiquetas. Nunca un filtro casero.
Errores comunes
- Limpiar al guardar en vez de al pintar. El mismo dato puede acabar en HTML, en un atributo, dentro de JavaScript o en una URL, y cada contexto necesita un escapado distinto. Por eso se escapa al salir.
- Confiar en datos "internos". Un nombre de producto que puso un compañero puede llevar un
<script>igual que uno de fuera. - Filtrar por listas negras. Bloquear la palabra
scriptno sirve: hay decenas de formas de ejecutar código sin escribirla. - Meter datos del servidor dentro de un bloque de JavaScript sin codificarlos correctamente.
innerHTMLpor comodidad. Es la causa más frecuente del XSS basado en DOM.
Cuándo preocuparte
Siempre que muestres en pantalla algo que no hayas escrito tú: comentarios, nombres, mensajes, descripciones, resultados de búsqueda, parámetros de la dirección, correos.
Y con especial cuidado en un panel de administración: ahí un XSS ejecuta con permisos de administrador, que es el peor escenario posible.