Cuando tu página incluye en su HTML un texto que escribió otra persona sin tratarlo, el navegador no distingue entre tu código y el ajeno. Lo ejecuta igual, con los permisos de tu sitio. Eso es el cross-site scripting, XSS, y permite actuar en nombre del usuario, leer lo que ve en pantalla o redirigirlo a otro lado.
Los tres tipos
Reflejado. El dato llega en la petición y vuelve en la respuesta, sin guardarse. Un ejemplo de manual es una página de búsqueda que muestra "Resultados para: " seguido de lo que vino en la URL. Si ese texto se inserta sin codificar y contiene etiquetas HTML, el navegador las interpreta. El ataque requiere que la víctima abra un enlace armado con ese contenido.
Almacenado. El dato se guarda (un comentario, un nombre de perfil, un mensaje) y se muestra después a otros usuarios. No hace falta engañar a nadie para que haga clic: basta con que visite la página. Suele ser el más grave, sobre todo si lo ve un administrador.
Basado en DOM. El problema está en el JavaScript del propio cliente, que toma un dato (la parte de la URL, el almacenamiento local) y lo escribe en la página con una función peligrosa. El servidor puede ni enterarse.
Un ejemplo mínimo de este último, vulnerable:
// VULNERABLE: interpreta el texto como HTML
const nombre = new URLSearchParams(location.search).get("nombre");
document.getElementById("saludo").innerHTML = "Hola " + nombre;
Y su versión corregida:
const nombre = new URLSearchParams(location.search).get("nombre");
document.getElementById("saludo").textContent = "Hola " + nombre;
textContent trata el valor como texto plano, sin interpretarlo como marcado. La diferencia entre las dos líneas es toda la defensa.
Codificar la salida según el contexto
La regla central es codificar los datos al momento de escribirlos en la página, y hacerlo de la forma que corresponde al lugar donde caen. No es lo mismo un dato dentro del cuerpo de HTML, dentro de un atributo, dentro de un bloque de JavaScript o dentro de una URL: cada contexto tiene sus propios caracteres peligrosos.
Por eso conviene no armar esa codificación a mano. Usá la función de escape de tu motor de plantillas, y asegurate de que esté activa para cada variable. Y evitá, cuando se pueda, meter datos dinámicos dentro de bloques de script o de atributos de eventos.
La validación de entrada ayuda, pero no es la defensa principal: un campo de comentarios tiene que aceptar texto con símbolos de menor y mayor, porque son legítimos. Lo correcto es guardarlos como vienen y codificar al mostrar.
Frameworks y sus excepciones
La mayoría de los frameworks modernos de interfaz escapan por defecto el contenido que insertás con la sintaxis habitual. Eso eliminó muchísimos casos. Pero todos ofrecen una salida para insertar HTML sin escapar, con nombres que a menudo advierten del peligro. Cada vez que aparezca uno de esos usos en una revisión de código, hacé tres preguntas: de dónde viene ese dato, quién lo puede modificar, y si realmente hace falta HTML.
Si necesitás permitir formato (negritas, listas en un editor de texto enriquecido), pasá el contenido por una biblioteca de saneamiento mantenida, con una lista acotada de etiquetas permitidas. No escribas tu propio filtro con expresiones regulares.
Capas extra
Content Security Policy. Es una cabecera que le dice al navegador de dónde puede cargar y ejecutar scripts. Bien configurada, puede impedir la ejecución de código inyectado aun cuando la codificación falle. Es una segunda barrera, no un reemplazo: una política laxa (que permita scripts en línea de cualquier origen) casi no aporta. Conviene introducirla en modo de solo reporte primero, para ver qué rompería.
Cookies HttpOnly. Marcar la cookie de sesión como HttpOnly impide que el JavaScript de la página la lea. Un XSS sigue pudiendo actuar en nombre del usuario mientras está en la página, pero no puede llevarse la sesión.
Cómo detectarlo
En revisión de código, buscá usos de las funciones que insertan HTML sin escapar y de los mecanismos que desactivan el escape de la plantilla. En pruebas, ingresá en tus propios formularios (en un ambiente de prueba) texto con caracteres especiales y observá cómo se muestra: si aparecen tal cual, bien; si cambia el formato de la página, hay un problema. Los escáneres automáticos encuentran bien los casos reflejados simples, y menos los de DOM, que requieren análisis del código del cliente.