Envenenamiento de caché en paquetes populares de código abierto
Adam Goldschmidt
18 de enero de 2021
0 minutos de lecturaA raíz de la investigación de James Kettle, de PortSwigger, sobre el envenenamiento de caché web, el equipo de seguridad de Snyk decidió profundizar nuestros conocimientos en este campo y explorar estas vulnerabilidades en el ámbito del código abierto. Centramos nuestra investigación en los frameworks web más populares de npm y PyPI, como Flask (Werkzeug), Bottle, Tornado y DerbyJS.
Esta publicación presenta el envenenamiento de caché web y muestra por qué los mantenedores de código abierto deben tener en cuenta este problema. Además, este blog presenta ejemplos de vulnerabilidades en frameworks de código abierto conocidos que descubrimos vulnerables durante la investigación inicial de Snyk.
Explicación del envenenamiento de caché
El envenenamiento de caché web es un ataque diseñado para engañar a la caché y hacer que sirva respuestas maliciosas a solicitudes válidas. Es posible al incluir en la solicitud parámetros que no forman parte de la clave, que se guardan en la caché pero no se representan en la clave de caché (de ahí que no formen parte de ella). Para comprender por completo cómo funciona el ataque, primero hay que entender el concepto de almacenamiento en caché web.
¿Qué es un proxy de caché?
Un proxy de caché es parte de un proxy inverso: una conexión intermedia entre el cliente y el servidor web. Cuando una persona accede a un sitio web, los proxies interpretan y responden las solicitudes en nombre del servidor original. El almacenamiento en caché mediante proxy es una de las funciones de un proxy inverso y permite entregar respuestas más rápido al usuario.
¿Cómo funciona el almacenamiento en caché?
El almacenamiento en caché consiste en guardar el contenido al que se accede con frecuencia para agilizar las solicitudes posteriores de ese contenido.
Las claves de caché permiten que la caché mantenga referencias a las respuestas. Por lo general, una clave de caché consta de los valores de uno o más encabezados de respuesta y una parte de la URL.
Por ejemplo, para la siguiente solicitud HTTP, la clave de caché podría ser localhost/p/?a=1.

Al recibir una nueva solicitud, si la caché encuentra una clave coincidente, se sirve la respuesta guardada en lugar de generar una nueva.
Como se observa en el ejemplo anterior, hay encabezados que pueden afectar la respuesta, pero que no se reflejan en la clave de caché. Esto significa que, si cambiamos sus valores, la respuesta se guardará en el mismo «espacio» de la caché, pero con un valor diferente.
La siguiente tabla muestra cómo se procesan tres solicitudes diferentes con la clave de caché definida como $host$query_args:
Host | Accept-Encoding | Argumentos de consulta | Clave de caché |
|---|---|---|---|
example.com | gzip, deflate | ?q=search | example.com?q=search |
example.com | identity | ?q=search | example.com?q=search |
snyk.io | gzip, deflat | snyk.io |
Las dos primeras filas tienen la misma clave de caché a pesar de tener distintos valores de Accept-Encoding; por lo tanto, se almacenarán en el mismo espacio de la caché.
Comprender los parámetros que no forman parte de la clave

Las entradas que no forman parte de la clave de caché se denominan parámetros no incluidos en la clave. Esto supone un problema cuando estos parámetros pueden provocar un comportamiento malicioso en la aplicación. Por ejemplo, un atacante puede convertir una XSS reflejada en una XSS almacenada. Veamos esta solicitud como ejemplo:
Si suponemos que el parámetro Origin se refleja sin sanitizar, puede provocar una XSS y no forma parte de la clave, se servirá la respuesta maliciosa a cada usuario que visite somesite.com/ hasta que venza la respuesta almacenada en caché.
Para obtener más información sobre el envenenamiento de caché y los posibles vectores de ataque, recomiendo leer estas publicaciones de James Kettle: Practical Web Cache Poisoning y Web Cache Entanglement: Novel Pathways to Poisoning.
Exploración de vulnerabilidades en frameworks web
Un framework web permite que los desarrolladores generen respuestas HTTP con facilidad. Según Wikipedia: «Los frameworks web ofrecen una forma estándar de crear e implementar aplicaciones web en la World Wide Web. Su objetivo es automatizar las tareas adicionales asociadas con actividades comunes del desarrollo web». Por lo general, estos frameworks incluyen medidas de seguridad para facilitar aún más el trabajo de los desarrolladores.
Los desarrolladores suelen usar frameworks web junto con un proxy de caché, como NGINX o Varnish.
Esta investigación demuestra que muchos de los frameworks populares actuales son vulnerables al envenenamiento de caché web de forma predeterminada, casi sin importar qué proxy de caché se utilice, a menos que se configuren explícitamente para defenderse de este tipo de ataques. Por lo general, la mayoría de los desarrolladores no lo sabe o no tiene los conocimientos necesarios para hacerlo. Los siguientes vectores de ataque se probaron en varios frameworks web con NGINX y Varnish.
Ocultamiento de parámetros GET en Python y Bottle

Cuando el atacante puede separar los parámetros de consulta con un punto y coma ;, puede provocar una diferencia entre cómo interpreta la solicitud el proxy (que se ejecuta con la configuración predeterminada) y cómo la interpreta el servidor. Esto puede hacer que solicitudes maliciosas se almacenen en caché como si fueran completamente seguras, ya que el proxy normalmente no reconoce el punto y coma como separador y, por lo tanto, no lo incluiría en una clave de caché para un parámetro que no forma parte de ella, como los parámetros utm_*, que por lo general no se incluyen en la clave. La recomendación del W3C recomienda usar ampersands como separadores («Que las cadenas sean el resultado de dividir estrictamente la cadena de datos en los caracteres U+0026 AMPERSAND (&)»).
El hallazgo más destacado fue el código fuente de Python (CVE-2021-23336), que contiene un método llamado parse_qsl que analiza los parámetros de consulta de las URL usando tanto punto y coma como ampersand. Luego, frameworks como Tornado usan este método para analizar los parámetros de consulta, lo que puede dar lugar a una cadena de explotación de envenenamiento de caché web.
Se descubrió que Bottle (CVE-2020-28473), Tornado y Rack eran vulnerables. Veamos un ejemplo de este vector que explota Bottle.
El atacante usa q=cat como parámetro del cuadro de búsqueda y lo reemplaza por otro valor. Estas son la solicitud y la respuesta:
Ahora supongamos que un usuario real busca «cat» mientras la respuesta maliciosa sigue almacenada en caché:
El atacante pudo modificar una solicitud legítima y reemplazar el parámetro de búsqueda. Esto se debe a que el servidor ve 3 parámetros: q, utm_content, y luego q otra vez. Reemplaza el valor del primer parámetro q por el del último. Por otro lado, el proxy considera que toda esta cadena: ?q=cat&utm_content=1;q=dog! es el valor de utm_content, por lo que la clave de caché solo contendría localhost?q=cat.
Para corregir esta vulnerabilidad, se debería usar únicamente el ampersand (&) como separador de parámetros de consulta, salvo que el desarrollador especifique lo contrario. Por ejemplo, Werkzeug permite que los desarrolladores especifiquen parámetros personalizados y usa el ampersand como opción predeterminada. Los mantenedores de Bottle decidieron corregir el problema al no dividir las cadenas de consulta por ;, un cambio introducido en la versión 0.12.19. También se descubrió que el framework Rails era vulnerable a este método (lo descubrió James Kettle y nos lo comunicó con ayuda de Jonathan Leitschuh), pero al momento de escribir esto todavía no se había corregido.
Vulnerabilidades de parámetros en el cuerpo de solicitudes GET (GET con cuerpo) en Flask y Tornado

En algunos proxies, como NGINX, es posible incluir parámetros en el cuerpo de una solicitud GET. Aunque esto no está estrictamente prohibido por la RFC de HTTP («Una carga útil dentro de un mensaje de solicitud GET no tiene semántica definida; enviar un cuerpo de carga útil en una solicitud GET podría hacer que algunas implementaciones existentes rechacen la solicitud»), se descubrió que varios frameworks incluyen estos parámetros en métodos integrados que no están diseñados explícitamente para parámetros del cuerpo.
Esto puede hacer que los desarrolladores intenten obtener parámetros de consulta GET, pero terminen recuperando parámetros del cuerpo. Estos parámetros no forman parte de la clave de caché, lo que puede ocasionar dos problemas:
Reemplazo de parámetros
Un atacante puede reemplazar los parámetros de consulta GET por parámetros del cuerpo GET y entregar la respuesta almacenada en caché a otros usuarios.
Este problema se encontró en Tornado cuando se usa con NGINX.
Como Tornado da prioridad a los parámetros del cuerpo, era posible reemplazar las solicitudes de usuarios desprevenidos por solicitudes maliciosas. Siguiendo el ejemplo anterior, al buscar «cat»:
Ahora, cuando un usuario busque «cat», este será el flujo:
En un escenario donde existe una vulnerabilidad de secuencias de comandos en sitios cruzados (XSS) reflejada, esta técnica podría convertirla en una XSS almacenada que se puede enviar a otros usuarios de la aplicación.
Inyección de parámetros adicionales
Un atacante puede inyectar parámetros adicionales y entregar la respuesta almacenada en caché a otros usuarios. Esto no es tan grave como la primera opción, pero aun así puede ser crítico si se combina con los elementos adecuados (por ejemplo, cambiar el método de solicitud usando _method en algunas implementaciones). Se demostró que esto era posible en Flask.
Esta solicitud haría que todas las solicitudes posteriores de usuarios desprevenidos incluyeran el parámetro adicional.
Snyk descubrió que varios frameworks permitían este comportamiento. Sin embargo, varios mantenedores a quienes contactamos no consideraron que esto fuera una vulnerabilidad directa del paquete. Por este motivo, Snyk decidió no publicar avisos sobre estos problemas.
Sin embargo, es posible corregir el problema en los propios paquetes si se impide que los desarrolladores obtengan datos del cuerpo mediante estos métodos ambiguos, ya que muchos usan request.params por comodidad, sin conocer las implicaciones. Otra solución es no dar prioridad a los parámetros del cuerpo al usar estos métodos ambiguos, ya que eso permite reemplazar parámetros de consulta legítimos.
Por ejemplo, los mantenedores de Werkzeug decidieron corregir esto impidiendo que request.values use request.form en solicitudes GET. Los mantenedores de Tornado optaron por otro enfoque: agregar una opción para que el análisis de los cuerpos de las solicitudes GET sea voluntario (aunque todavía no se había publicado).
Alcance de las correcciones
Durante esta investigación, identificamos numerosos casos en los que la complejidad de los distintos vectores de ataque hacía que los mantenedores no siempre entendieran si la corrección correspondía a la biblioteca que mantenían o si debía hacerse en el nivel del proxy.
Hay muchos escenarios distintos en los que puede ocurrir un envenenamiento de caché, y no es responsabilidad del framework web mitigarlos todos. Dicho esto, el framework web podría ayudar a proteger contra algunos de ellos si implementa medidas adicionales de defensa en profundidad.
Se podría argumentar que los proxies no deberían ignorar los parámetros del cuerpo GET, ya que muchas implementaciones aún los usan. Sin embargo, pueden almacenar estas claves en caché como si fueran parámetros de consulta. Además, los frameworks pueden impedir que los desarrolladores usen métodos ambiguos y, al mismo tiempo, permitirles usar parámetros del cuerpo. Lo mismo aplica al ocultamiento de parámetros: los proxies no deberían usar puntos y coma como separadores porque la RFC no lo recomienda, pero los frameworks pueden permitirlo solo si el desarrollador lo define explícitamente.
Cómo reducir el riesgo de envenenamiento de caché como desarrollador
Los desarrolladores pueden reducir el riesgo de vulnerabilidad si siguen estos puntos:
Ten en cuenta la clave de caché: Si tu servidor divide los argumentos de consulta con un punto y coma, asegúrate de que tu proxy de caché haga lo mismo. Además, asegúrate de que la clave de caché incluya los encabezados necesarios para impedir que los atacantes usen parámetros que no forman parte de la clave para envenenar la caché web.
Ignora los parámetros del cuerpo GET, salvo que sean necesarios para el flujo del programa. Si lo son, asegúrate de usarlos solo cuando haga falta.
Detecta y corrige otras vulnerabilidades en tu aplicación:El envenenamiento de caché web suele utilizarse como parte de una cadena de explotación, en la que un atacante puede enviar una respuesta maliciosa a otros usuarios; por ejemplo, convertir una XSS reflejada en una almacenada. Los desarrolladores deben hacer todo lo posible para proteger sus aplicaciones contra estas vulnerabilidades comunes, aunque parezcan menos graves.
Resumen
En conclusión, esta investigación demuestra que los frameworks de código abierto son vulnerables a ataques de envenenamiento de caché web casi sin importar qué proxy se utilice (salvo en algunos casos). Aunque es posible mitigar estos ataques en el proxy, muchos desarrolladores desconocen estos vectores de ataque y no implementan las medidas de protección necesarias en la caché o el proxy.
El objetivo de esta publicación era generar conciencia en la comunidad de desarrolladores. Aunque solo presenta dos posibles vectores de envenenamiento de caché web, hay muchos más en circulación. Los desarrolladores deben procurar seguir los puntos mencionados y tener siempre presentes estas vulnerabilidades poco comunes.
Empieza con los desafíos de Capture the Flag
Aprende a resolver desafíos de captura la bandera con nuestro taller virtual introductorio a pedido.
