Cookies envenenadas: cuando el "último producto visto" esconde un DOM-based XSS

Manipulación de cookies vía DOM

🔍 Un XSS que no está donde lo buscas

Hoy quiero compartir un caso muy didáctico de DOM-based cookie manipulation, una variante de XSS que no salta a la vista en el primer análisis de peticiones porque el problema no está en un parámetro reflejado directamente en el HTML, sino en una cookie que el propio JavaScript de la página construye a partir de datos que el usuario controla.

Este tipo de vulnerabilidad aparece a menudo en funcionalidades de «conveniencia» tipo e-commerce: recordar el último producto visitado, el último filtro aplicado, la última búsqueda realizada… Cualquier feature que guarde «algo que hiciste» y luego lo reutilice para pintar un enlace o un mensaje es candidata a revisión.

🕵️ Escenario

Al navegar por una tienda online, cada vez que visitamos la ficha de un producto observamos que se crea (o actualiza) una cookie en el navegador:

Cookie: session=xxxxxxxxxxxxxxxxxxxxxxxx; lastViewedProduct=https://tienda.ejemplo.com/product?productId=2

Y al volver a la página principal, aparece un enlace nuevo:

<a href='https://tienda.ejemplo.com/product?productId=2'>Last viewed product</a>

A simple vista parece inofensivo: la aplicación simplemente recuerda la URL del último producto y la muestra como acceso rápido. El problema aparece en cuanto nos preguntamos de dónde saca esa cookie su valor. Si al visitar un producto el script del cliente construye lastViewedProduct a partir de window.location.href o de un atributo similar, sin ningún tipo de codificación o validación, esa cookie es en realidad un reflejo casi literal de la URL que el atacante controla.

💡 Paso 1 — Confirmar la inyección en la cookie

El primer paso es comprobar si podemos «romper» el contexto HTML donde luego se vuelca esa cookie. Si la página que imprime el enlace hace algo parecido a:

<a href='COOKIE_VALUE'>Last viewed product</a>

sin escapar comillas simples ni caracteres especiales, podemos cerrar el atributo href y el propio tag <a> inyectando nuestro propio HTML dentro del valor de la URL:

https://tienda.ejemplo.com/product?productId=3&'><h1>hola</h1>

Si al visitar esa URL y volver al inicio vemos el <h1> renderizado en la página, ya tenemos confirmada la inyección: la cookie lastViewedProduct se está generando sin ningún tipo de sanitización a partir de la URL actual, y su valor se vuelca después en el DOM sin codificar.

💡 Paso 2 — De HTML roto a ejecución de JavaScript

Una vez confirmado que podemos inyectar markup arbitrario, el siguiente paso natural es sustituir el <h1> por un vector que ejecute JavaScript:

https://tienda.ejemplo.com/product?productId=3&'><script>print()</script>

Se usa print() como prueba de concepto inofensiva y fácilmente verificable (dispara el diálogo de impresión del navegador) en lugar de un payload que exfiltre datos, algo especialmente recomendable en entornos de laboratorio o pentest controlado.

🚫 El matiz que complica el ataque: dos pasos, dos páginas

Aquí está la parte más interesante del caso. El fallo tiene una peculiaridad: la cookie se envenena al visitar la página del producto, pero el payload solo se ejecuta al visitar la página principal, que es donde se lee esa cookie y se vuelca en el DOM. Un único <script> o una sola petición no bastan: hace falta encadenar dos navegaciones distintas de la víctima, en el orden correcto.

Para forzar esa secuencia sin depender de que la víctima haga dos clics por su cuenta, se puede recurrir a un servidor de exploit propio (habitual en laboratorios de formación tipo Web Security Academy) y montar una página intermedia con un <iframe> que haga el trabajo en dos tiempos:

<iframe
  width="600" height="600"
  src="https://tienda.ejemplo.com/product?productId=3&'><script>print()</script>"
  onload="if(!window.x){this.src='https://tienda.ejemplo.com/'; window.x=1}"
></iframe>

La lógica es la siguiente:

  • Primera carga del iframe: apunta directamente a la ficha del producto con el payload inyectado en la URL. Esto hace que el navegador de la víctima genere/actualice la cookie lastViewedProduct ya «envenenada» con nuestro <script> dentro.
  • onload: en cuanto esa primera carga termina, el propio evento cambia el src del iframe hacia la página principal.
  • Segunda carga del iframe: ahora la home lee la cookie (ya contaminada) y la vuelca sin escapar en el enlace «Last viewed product», ejecutando el <script>.
  • La bandera window.x evita que el onload se dispare en bucle infinito, ya que la segunda carga del iframe también lanza su propio evento onload.

Si se apuntara el iframe directamente a la home, el payload nunca llegaría a ejecutarse: la cookie todavía no estaría contaminada en ese primer instante. El orden de las dos navegaciones no es un detalle menor, es la clave de todo el ataque.

✅ Buenas prácticas para no caer en esto

  • Nunca confiar en document.cookie, window.location ni ningún dato controlado por el cliente a la hora de generar HTML dinámicamente. Todo dato que salga de la URL, un formulario o una cookie debe tratarse como entrada no confiable, venga o no de una petición al servidor.
  • Codificar siempre al insertar en el DOM. Usar textContent en lugar de innerHTML cuando sea posible, o funciones de codificación de atributos/HTML cuando el contenido deba insertarse como markup.
  • No construir cookies «de conveniencia» a partir de URLs completas sin validar. Si solo se necesita el productId, guardar el identificador numérico y reconstruir la URL en el servidor, nunca persistir la URL cruda tal cual llega del navegador.
  • Cabecera Content-Security-Policy como capa adicional: aunque no sustituye a una correcta sanitización, limita mucho el impacto si algún <script> inline llega a colarse.
  • Cookies sensibles con HttpOnly cuando no necesiten ser leídas por JavaScript, para reducir la superficie de robo vía XSS aunque este tipo concreto de fallo (manipulación, no robo directo) no dependa de ese flag.

🔐 Conclusión

Lo que hace interesante este caso no es la técnica de XSS en sí —una inyección clásica por falta de escapado—, sino dónde vive el problema: no en una respuesta del servidor, sino en JavaScript del lado del cliente que confía ciegamente en la URL actual para construir una cookie, y en otra parte de la aplicación que confía igual de ciegamente en esa cookie para pintar HTML. Cuando el análisis de peticiones HTTP «no muestra nada raro», vale la pena mirar también qué hace el JavaScript en el navegador con los datos que ya tiene delante: cookies, location, referrer, postMessage… El DOM también es superficie de ataque.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio