Saltearse al contenido

Cross-Site Scripting (XSS)

XSS = ejecución de código del atacante en el navegador de la víctima, dentro del origen de la aplicación. No se vulnera el servidor; se vulnera la confianza del navegador en el código que la app le entrega. Como ese código corre en el origen de la víctima, hereda todo su poder en el cliente: leer y modificar el DOM, usar sus cookies y tokens, lanzar peticiones autenticadas y suplantar la interfaz. Un XSS almacenado en un panel de administración es, en la práctica, compromiso de la plataforma.

Lo que gana el atacante depende de dónde dispare el XSS y qué protecciones existan:

  • Sin HttpOnly → robo directo de cookie de sesión (document.cookie).
  • Con HttpOnly → no lee la cookie, pero actúa en nombre de la víctima (las cookies viajan solas en las peticiones del propio navegador).
  • Tokens en localStorage/sessionStorage → accesibles siempre desde JS.
  • Panel interno / admin (stored o blind) → creación de usuarios, cambios de configuración, pivote.

El navegador es un intérprete de varios lenguajes (HTML, JS, CSS, URL). El XSS nace cuando un dato controlable se coloca en una posición donde el parser lo trata como código, porque la app no lo ha codificado para ese contexto ni sanitizado. La Same-Origin Policy aísla orígenes, pero el payload se ejecuta dentro del origen vulnerable, así que la SOP juega a favor del atacante.

En el XSS basado en DOM el flujo clave es source → sink:

  • Sources (entrada controlable): location (.href/.search/.hash/.pathname), document.referrer, document.cookie, window.name, postMessage, localStorage, respuestas de API reflejadas en el DOM.
  • Sinks (ejecución): eval, Function, setTimeout/setInterval con string, element.innerHTML/outerHTML, document.write/writeln, insertAdjacentHTML, element.setAttribute (de href/src/on*), location/location.href, iframe.srcdoc, script.src/text, y en jQuery $(), .html(), .append().

El payload válido depende exclusivamente del contexto donde cae el dato:

  1. Entre etiquetas <div>AQUÍ… (entre etiquetas) → inyectar una etiqueta con ejecución.
  2. Atributo entre comillas value="AQUÍ" → cerrar comillas + handler, o autofocus onfocus=.
  3. Atributo sin comillas value=AQUÍ → basta un espacio para añadir atributos.
  4. Dentro de JavaScript var x='AQUÍ' → cerrar la cadena/sentencia, o romper un template literal `...${AQUÍ}...`.
  5. En una URL href="AQUÍ" → esquema javascript:.
  6. En CSS style="AQUÍ" → expression() (legacy), exfiltración por background:url().
  • Reflejado: el payload va en la petición (query, cabecera, cuerpo) y vuelve en la respuesta inmediata. Requiere entregar un enlace/formulario a la víctima.
  • Almacenado (persistente): el payload se guarda (comentario, perfil, nombre, ticket, metadatos de fichero) y se sirve a cada visitante. El de mayor impacto; en apps sociales puede auto-propagarse (worm, p. ej. Samy).
  • DOM-based: la inyección ocurre solo en cliente; el HTML del servidor puede ser inofensivo. La codificación de salida en servidor no la mitiga.
  • Blind: se ejecuta donde no lo ves (visor de tickets, logs, backoffice). Se detecta y explota out-of-band.
  • mXSS (mutation): el sanitizador emite HTML “seguro” que el navegador re-parsea al normalizar el DOM (confusión de namespaces HTML/SVG/MathML, noscript, template), resucitando el vector. Rompe sanitizadores mal diseñados.
  • Self-XSS: requiere que la víctima pegue el payload; de bajo impacto salvo que se escale combinándolo con clickjacking o ingeniería social.
  • UXSS: fallo del navegador o una extensión que rompe la SOP; afecta a cualquier sitio.
  • Inyecta un marcador único (pwn7h3) en cada parámetro GET/POST, fragmento, cabecera (Referer, User-Agent, X-Forwarded-*), cookie y campo JSON. Localiza todos los reflejos y clasifica el contexto de cada uno antes de tocar un solo payload.
  • Prueba los caracteres de ruptura del contexto y observa si vuelven sin codificar: < > " ' / = { }`.
  • DOM XSS: con DOM Invader (Burp) o a mano en las DevTools, traza source→sink; busca innerHTML, eval, sinks de framework, postMessage sin validar origin.
  • Blind XSS: planta sondas con callback OOB en nombre, dirección, user-agent, campos de soporte/CRM; espera la ejecución en el backoffice.

�0�

  1. Levanta un receptor en una IP que la víctima pueda alcanzar: �1�
  2. Inyecta el vector en el punto reflejado o almacenado: �2�
  3. Al renderizarse la página (la víctima, o un admin en stored/blind), recibes la cookie en tu log.
  4. Si es HttpOnly, actúa en su nombre desde su navegador (la cookie viaja sola): �3�
  5. Para XSS ciego, usa un dominio OOB y espera la ejecución asíncrona.
  • Robo de credenciales de sesión: document.cookie, tokens de localStorage.
  • Acciones autenticadas con fetch/XHR (credentials:'include'): cambiar email/contraseña (→ ATO), crear usuarios, aprobar transacciones.
  • Robo del token anti-CSRF leyendo el DOM/respuestas, para encadenar.
  • Keylogging y captura de formularios (addEventListener('input'...)).
  • Phishing in-page (sobreponer un login falso en el dominio legítimo).
  • Escaneo de red interna y SSRF desde el cliente; en apps de escritorio (Electron) un XSS puede escalar a RCE vía nodeIntegration.
  • Persistencia registrando un service worker malicioso.
  • Codificaciones: entidades HTML (&#x61;, &#97;), URL y doble-URL, unicode (a), hex en JS, anidadas. El navegador decodifica según el contexto, el filtro a menudo no.
  • Ofuscación de etiquetas/handlers: mayúsculas mixtas, etiquetas poco comunes (<svg>, <math>, <marquee>), handlers alternativos (onpointerover, ontoggle, onanimationstart, onfocus + autofocus), separadores (/, %0a, %0c), atributos sin comillas.
  • Sin palabras clave bloqueadas: import(name) en vez de eval, construir cadenas con String.fromCharCode, atob, concatenación.
  • Quirks del parser HTML: comentarios mal cerrados, < sin cerrar, foreign content (SVG/MathML) que cambia las reglas de parsing (base de mXSS).

Una CSP mal diseñada no detiene el XSS:

  • unsafe-inline o whitelist de CDNs grandes → casi siempre eludible.
  • Endpoints JSONP en dominios permitidos → cargar tu callback.
  • Script gadgets: librerías permitidas con eval/plantillas (AngularJS ng-app, Vue en modo template) ejecutan expresiones.
  • base-uri sin restringir → secuestrar rutas relativas de <script>.
  • nonce filtrado/reutilizado o strict-dynamic que carga un gadget.
  • Dangling markup / exfiltración: cuando no puedes ejecutar, roba datos del DOM con una etiqueta colgante (<img src='//oob.tld?), DNS-prefetch, o <link>.

Verifica la política con CSP Evaluator y busca gadgets conocidos.

  • Ficheros subidos: SVG con <script>, HTML servido inline, nombres de fichero reflejados, metadatos EXIF reflejados.
  • Generadores de PDF / conversores HTML→PDF: XSS → SSRF/lectura de ficheros locales en el servidor de render.
  • Markdown / editores WYSIWYG: HTML crudo permitido, javascript: en enlaces.
  • Cabeceras y errores: XSS reflejado en páginas de error o en respuestas que reflejan cabeceras.
  • postMessage: receptores que hacen innerHTML del event.data sin validar event.origin.

Burp Suite (+DOM Invader), dalfox, Gxss/kxss, arjun/paramspider (descubrir parámetros), XSS Hunter self-hosted / interactsh (blind/OOB), BeEF (demostrar post-explotación), CSP Evaluator (auditar política).

Exfiltra por un canal propio discreto (beacon de imagen, fetch keepalive, DNS), evita alert ruidosos, usa dominios OOB neutros, y ciñe el alcance y los datos a lo pactado en el engagement.

  • CSP en modo report (Content-Security-Policy-Report-Only + report-to/ report-uri): las violaciones delatan scripts no autorizados e inyecciones en producción, incluso de DOM XSS.
  • WAF/IDS: firmas (<script, onerror=, javascript:, srcdoc) más detección por anomalía (longitud, entropía, codificaciones anidadas) — las firmas solas se evaden.
  • SIEM: correlaciona entrada con metacaracteres HTML que luego se refleja en respuestas 200; picos de peticiones salientes a dominios desconocidos (beacons) desde sesiones de usuario; para blind interno, alertar si el backoffice llama a dominios externos no catalogados.
  • Honeytokens: cookies/campos señuelo que, si se exfiltran, disparan alerta.

Reportes CSP, logs de acceso con body/parámetros, proxy/egress, RASP o WAF con inspección de respuesta, y verificación de integridad del contenido almacenado.

  1. Codificación de salida por contexto — mitigación raíz. Usa la del framework (autoescape de React/Angular/Vue, t() seguras) y la de OWASP (Java Encoder, etc.). El riesgo vuelve con dangerouslySetInnerHTML (React), v-html (Vue), bypassSecurityTrust* (Angular): si hay que renderizar HTML del usuario, sanitiza con DOMPurify con config restrictiva.
  2. CSP basada en nonce/hash con strict-dynamic, sin unsafe-inline, con base-uri 'none' y object-src 'none'. Despliega primero en Report-Only.
  3. Trusted Types (require-trusted-types-for 'script') para cerrar los sinks de DOM XSS en navegadores Chromium.
  4. Cookies HttpOnly + Secure + SameSite; no guardar tokens de sesión en localStorage.
  5. Validación de entrada por lista de permitidos como capa extra (nunca única).

Purga el payload almacenado, invalida y rota sesiones/tokens afectados, revisa qué cuentas pudieron comprometerse (correos/contraseñas cambiados, usuarios creados), añade regla de detección específica y haz post-mortem del punto de inyección.

  • Samy (MySpace, 2005) — XSS almacenado que se autoreplicaba añadiendo al atacante como amigo; ~1 millón de perfiles en 20 horas. El worm XSS más citado de la historia.
  • Twitter “onMouseOver” (2010) — XSS almacenado en el timeline: pasar el ratón por un tweet ejecutaba JS y lo auto-retwitteaba; se propagó en minutos.
  • TweetDeck (2014) — XSS almacenado en el render de tweets; un único tweet con el payload se auto-retwitteó de forma masiva.
  • British Airways / Magecart (2018) — inyección de JS malicioso en el cliente (skimmer) que exfiltró datos de pago de ~380.000 transacciones. Ejemplo del impacto de ejecutar JavaScript no confiable en el origen de la víctima.
  • Bypasses de sanitizadores (mXSS) — DOMPurify y otros han publicado múltiples advisories por mutation XSS; mantener el sanitizador actualizado es parte de la defensa.

Para CVEs concretos por producto, consulta los feeds oficiales: NVD (https://nvd.nist.gov/vuln/search) y GitHub Security Advisories (https://github.com/advisories). La mayoría de XSS reales se publican como CVE del componente afectado.

  • Reflejos de cada parámetro/cabecera/cookie, con su contexto identificado.
  • Prueba por contexto: etiqueta, atributo (con/sin comillas), JS, URL, CSS.
  • DOM XSS: source→sink revisados (incl. postMessage).
  • Blind: sondas OOB en todos los campos que acaben en un panel interno.
  • Si hay CSP: evaluada y probados gadgets/JSONP/base-uri.
  • Impacto demostrado (cookie/acción autenticada) y documentado con PoC mínima.