Volver al glosario

Cross-site scripting (XSS)

8 min de lectura

En seguridad de aplicaciones web, el cross-site scripting (XSS) es un defecto que permite a un atacante ejecutar su propio JavaScript en el navegador de otro usuario, dentro de la frontera de confianza de tu sitio. El navegador no sabe distinguir el script inyectado del tuyo, así que el script hereda la sesión de ese usuario, sus cookies y todo lo que la aplicación le deja hacer.

24 de julio de 2026
Compartir:

Cómo funciona

El cross-site scripting ocurre cuando un valor que vino de fuera de la aplicación acaba en la página como código en vez de como contenido. El navegador no tiene forma de saber que una etiqueta de script llegó desde un parámetro de la URL y no desde quien programó, así que la ejecuta con toda la autoridad de la página: mismo origen, mismas cookies, mismo acceso al DOM y a todas las API que el usuario que ha iniciado sesión tiene derecho a llamar.

El mecanismo son siempre tres pasos. Un valor no confiable entra en la aplicación, por un parámetro de la URL, un campo de formulario, una cabecera HTTP, un registro de la base de datos o un mensaje de otro sistema. Viaja hasta un sitio de la página donde el navegador lo va a interpretar. Y se escribe allí sin codificarlo para ese sitio concreto.

Ese último paso es donde falla la mayoría de las implementaciones, porque codificar no es una sola cosa. Un valor que es seguro dentro de un nodo de texto HTML no lo es dentro de un atributo, dentro de un bloque de script, dentro de una URL o dentro de una declaración CSS. Cada uno de esos es un contexto de interpretación distinto con un escapado distinto, y una sola función de escapado aplicada a todo se equivocará por lo menos en uno.

Lo que se lleva el atacante no es una ventanita de aviso. En cuanto el script se ejecuta, es el usuario. Lee el DOM, envía formularios en su nombre, llama a las API que alcanza esa sesión y, si la cookie de sesión no está marcada como HttpOnly, lee la cookie directamente. Donde la aplicación ofrece cambiar el correo o la contraseña por autoservicio, la distancia entre el XSS y el robo de la cuenta es una petición.

Qué sale mal

El fallo que encontramos una y otra vez no es una aplicación sin codificación. Es una aplicación con la codificación elegida para el contexto equivocado. Un framework escapa bien el HTML en todos los sitios donde pinta texto, y después una plantilla mete un valor en un atributo de manejador de eventos o construye un elemento con innerHTML, y ese único sitio decide el hallazgo.

El XSS basado en DOM es la variante que sobrevive más tiempo, porque no aparece nunca en la respuesta del servidor. La carga puede ir en el fragmento que hay detrás de la almohadilla, que los navegadores no mandan al servidor en absoluto, así que nada en los registros de acceso ni en un filtro del lado del servidor llega a verlo. Lo buscamos leyendo el paquete de cliente en busca de los sumideros, no haciendo fuzzing contra el servidor.

El XSS almacenado es el que cambia la forma de un proyecto. Una carga reflejada necesita que la víctima siga un enlace. Una carga almacenada espera en un ticket de soporte, en el nombre de un dispositivo, en un campo de perfil o en un registro de auditoría hasta que un usuario privilegiado abre la página que lo pinta. En las aplicaciones internas, la ruta fiable hasta un administrador rara vez es el formulario de inicio de sesión: es un campo que el administrador está obligado a leer para hacer su trabajo. Apuntamos las cargas a las vistas de trastienda justo por eso, y el hallazgo que escribimos no es «hay XSS en el asunto del ticket», sino «un usuario corriente puede ejecutar script en la consola de quien revisa la cola».

XSS reflejado, almacenado y basado en DOM

Los tres nombres describen dónde vive la carga, no lo que el script puede hacer. Una vez se ejecuta, la capacidad es la misma. Lo que cambia es a quién se puede alcanzar y qué evidencia queda después.

Reflejado Almacenado Basado en DOM
Dónde vive la carga En la petición, devuelta tal cual En el almacenamiento de la propia aplicación Solo en el código de cliente
Qué hace falta para que dispare Que la víctima siga un enlace preparado Que la víctima abra una página normal Que la víctima abra una URL o un estado preparados
A quién alcanza A un objetivo cada vez A todo el que vea el registro A un objetivo cada vez
Se ve en los registros del servidor Sí, al escribirse Muchas veces no, si viaja en el fragmento
Dónde se arregla Codificando la salida en la respuesta del servidor Codificando al pintar, no saneando en la entrada En el sumidero del cliente, en JavaScript

La consecuencia práctica es de severidad, no de taxonomía. Un XSS almacenado en una vista que solo abren los administradores vale más que un XSS reflejado en cualquier sitio, porque no necesita ingeniería social y apunta exactamente a la cuenta que quiere el atacante.

Errores frecuentes

Sanear en la entrada en vez de codificar en la salida. La representación segura depende de dónde se pinte el valor, y eso no se sabe cuando entra. Limpiar en la puerta corrompe además datos legítimos y da una falsa sensación de cobertura.

Tomarse un web application firewall como el arreglo. Los filtros de peticiones encarecen una carga y no abaratan ninguna otra cosa. Merecen la pena como retraso, y no son un control que puedas enseñarle a un auditor mientras el sumidero siga ahí.

Dar por hecho que HttpOnly lo cierra. La marca impide que el script lea la cookie. No impide que el script use la cookie: las peticiones que salen de la página siguen llevándola. HttpOnly rebaja el robo de cookie a secuestro de sesión ejecutado dentro del navegador de la víctima, que para el atacante suele bastar.

Escribirse un saneador a medida. La interpretación de HTML está llena de casos límite que los navegadores resuelven de forma distinta, y los problemas de mutación convierten en marcado un texto que parecía inerte cuando el DOM lo vuelve a serializar. Usa una biblioteca mantenida o no aceptes HTML.

Publicar una Content Security Policy con unsafe-inline. Esa sola palabra quita casi todo aquello para lo que se añadió la política, y es el estado en el que más veces nos la encontramos.

Cómo reducirlo

Pinta a través de un motor de plantillas que codifique por defecto y trata cada sitio donde te lo saltas como una decisión que necesita un motivo. En el cliente, prefiere textContent a innerHTML, y donde de verdad haya que construir marcado, ponlo detrás de un saneador y detrás de Trusted Types, para que el navegador rechace las cadenas crudas en el sumidero.

Después añade una Content Security Policy como segunda capa, construida con nonces o hashes en vez de con una lista de máquinas permitidas, y sin unsafe-inline. Una política en modo solo informe durante una versión o dos te dice qué se rompería antes de que se rompa. Marca las cookies de sesión como HttpOnly, Secure y SameSite, que es el mismo conjunto de atributos de seguridad de cookie que limita lo que se puede llevar una carga que tenga éxito.

Para la detección, un escaneo automático de DAST encuentra los casos reflejados barato y no encuentra casi ninguno de los basados en DOM, porque el sumidero está en código que no lee. Revisa el paquete de cliente en busca de los sumideros directamente, y registra los informes de violación de CSP: en una política que lleva un tiempo estable, un estallido súbito de violaciones de script en línea en una sola ruta es una señal de verdad.

Dónde aparece esto en una auditoría

En un informe de aplicación web, el XSS se escribe por sumidero y por contexto, nunca una vez por aplicación. La reproducción que entregamos es la petición exacta o el estado de cliente exacto, el campo, el contexto de pintado que hizo que la carga se ejecutara, y una captura de la consecuencia y no de una ventanita de aviso. Donde la carga llega a una vista privilegiada, decimos a qué rol y a qué pantalla, porque eso es lo que mueve la severidad.

La severidad la deciden el alcance y la persistencia, no la clase. Un caso reflejado detrás de un inicio de sesión que solo afecta a quien hace clic es un hallazgo corriente. Un caso almacenado que un agente de soporte dispara haciendo su trabajo, en una sesión con derechos elevados, es el hallazgo alrededor del cual se construye el informe entero.

Esto es una de las cosas que buscamos cuando probamos una aplicación web a mano.

Preguntas frecuentes

¿Sigue siendo el XSS un riesgo real en 2026? Sí. Los frameworks modernos codifican por defecto, y eso retiró los casos fáciles y desplazó los que quedan al código de cliente, a los widgets de terceros y a los sitios donde quien programa decide expresamente saltarse el escapado. Lo seguimos encontrando en la mayoría de las aplicaciones con una trastienda rica.

¿Una Content Security Policy detiene el XSS? Más que detenerlo, lo contiene. Una política estricta basada en nonce impide que se ejecute la mayor parte del script inyectado, lo que convierte un hallazgo crítico en uno menor, pero el punto de inyección sigue ahí y las políticas se deterioran. Es una segunda capa, no el arreglo.

¿Cuál es la diferencia entre XSS y CSRF? El XSS ejecuta código del atacante dentro de tu origen. El cross-site request forgery no ejecuta código ninguno: hace que el navegador de la víctima mande una petición cuya respuesta el atacante no puede leer. El XSS es estrictamente más potente, y derrota a los tokens de CSRF porque puede leerlos.

¿Por qué no lo encontró nuestro escáner? Los escáneres prueban la respuesta del servidor. Los casos basados en DOM no llegan nunca al servidor, los almacenados necesitan que el escáner alcance la página que pinta el registro, y los dos necesitan una sesión privilegiada válida. Un escaneo limpio es evidencia solo sobre los casos reflejados.

¿Quieres ver cómo trabajamos en Asperis Security?

Agenda 30 minutos con uno de nuestros especialistas. Revisamos tu stack y te decimos qué conviene probar primero.