Empoisonnement du cache dans des packages open source populaires
Adam Goldschmidt
18 janvier 2021
0 minutes de lectureÀ la suite des recherches de James Kettle, de PortSwigger, sur l’empoisonnement du cache Web, l’équipe sécurité de Snyk a décidé d’approfondir ses connaissances dans ce domaine et d’explorer ces vulnérabilités dans l’écosystème open source. Nous avons concentré nos recherches sur les frameworks Web les plus populaires de npm et PyPI, notamment Flask (Werkzeug), Bottle, Tornado et DerbyJS.
Cet article présente l’empoisonnement du cache Web et explique pourquoi les mainteneurs de projets open source doivent prendre cette menace en compte. Il présente également des exemples de vulnérabilités dans des frameworks open source connus, détectées lors des premières recherches de Snyk.
L’empoisonnement du cache expliqué
L’empoisonnement du cache Web est une attaque qui vise à tromper le cache pour qu’il renvoie des réponses malveillantes à des requêtes légitimes. Elle est rendue possible par l’inclusion dans la requête de paramètres non pris en compte dans la clé, qui sont enregistrés dans le cache sans être représentés dans cette clé (d’où leur nom de paramètres « non indexés »). Pour bien comprendre le fonctionnement de cette attaque, il faut d’abord comprendre le principe de la mise en cache Web.
Qu’est-ce qu’un proxy de cache ?
Un proxy de cache fait partie d’un proxy inverse : une connexion intermédiaire entre le client et le serveur Web. Lorsqu’un utilisateur accède à un site Web, les proxys interprètent les requêtes et y répondent au nom du serveur d’origine. La mise en cache par proxy est l’une des fonctionnalités d’un proxy inverse ; elle permet de transmettre les réponses plus rapidement à l’utilisateur.
Comment fonctionne la mise en cache ?
La mise en cache consiste à stocker les contenus fréquemment consultés afin d’accélérer les requêtes suivantes visant à y accéder.
Les clés de cache permettent au cache de référencer les réponses. En général, une clé de cache se compose des valeurs d’un ou de plusieurs en-têtes de réponse et d’une partie de l’URL.
Par exemple, pour la requête HTTP suivante, la clé de cache pourrait être localhost/p/?a=1.

Lorsqu’une nouvelle requête arrive, si le cache trouve une clé correspondante, il renvoie la réponse enregistrée au lieu d’en générer une nouvelle.
Comme le montre l’exemple ci-dessus, certains en-têtes peuvent influer sur la réponse sans être pris en compte dans la clé de cache. Ainsi, si nous modifions leurs valeurs, la réponse sera enregistrée au même emplacement dans le cache, mais avec une valeur différente.
Le tableau suivant montre comment trois requêtes différentes sont traitées avec une clé de cache définie comme $host$query_args :
Hôte | Accept-Encoding | Arguments de requête | Clé de cache |
|---|---|---|---|
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 |
Les deux premières lignes ont la même clé de cache malgré des valeurs différentes pour Accept-Encoding ; elles seront donc mises en cache au même emplacement.
Comprendre les paramètres non indexés

Les entrées qui ne font pas partie de la clé de cache sont appelées paramètres non indexés. Elles deviennent problématiques lorsqu’elles peuvent entraîner un comportement malveillant dans l’application. Par exemple, un attaquant peut transformer un XSS réfléchi en XSS stocké. Prenons la requête suivante :
Si le paramètre Origin est renvoyé sans assainissement, peut entraîner une attaque XSS et n’est pas indexé, chaque utilisateur qui consulte somesite.com/ recevra la réponse malveillante jusqu’à son expiration du cache.
Pour en savoir plus sur l’empoisonnement du cache et les vecteurs d’attaque possibles, je vous recommande les articles suivants de James Kettle : Practical Web Cache Poisoning et Web Cache Entanglement: Novel Pathways to Poisoning.
Exploration des vulnérabilités des frameworks Web
Un framework Web permet aux développeurs de générer facilement des réponses HTTP. Selon Wikipedia : « Les frameworks Web fournissent une méthode standard pour créer et déployer des applications Web sur le World Wide Web. Ils visent à automatiser les tâches récurrentes du développement Web. » En général, ces frameworks intègrent des mesures de sécurité pour simplifier encore davantage la vie des développeurs.
Les développeurs utilisent souvent des frameworks Web avec un proxy de cache, comme NGINX ou Varnish.
Cette étude montre que de nombreux frameworks populaires sont vulnérables par défaut à l’empoisonnement du cache Web, quel que soit le proxy de cache utilisé, à moins qu’il soit configuré explicitement pour se défendre contre ce type d’attaque. Or, la plupart des développeurs n’en ont généralement pas connaissance ou ne disposent pas des connaissances suffisantes pour le faire. Les vecteurs d’attaque suivants ont été testés sur plusieurs frameworks Web avec NGINX et Varnish.
Dissimulation de paramètres GET en Python et dans Bottle

Lorsqu’un attaquant peut séparer les paramètres de requête à l’aide d’un point-virgule ;, il peut provoquer une différence d’interprétation de la requête entre le proxy (avec sa configuration par défaut) et le serveur. Des requêtes malveillantes peuvent alors être mises en cache comme si elles étaient parfaitement sûres : en général, le proxy ne reconnaît pas le point-virgule comme séparateur et ne l’inclut donc pas dans la clé de cache d’un paramètre non indexé, comme les paramètres utm_*, qui ne sont généralement pas indexés. La recommandation du W3C préconise d’utiliser des esperluettes comme séparateurs (« Les chaînes sont obtenues en divisant strictement la chaîne de données à chaque caractère U+0026 ESPERLUETTE (&) »).
La découverte la plus notable concernait le code source de Python (CVE-2021-23336), qui contient une méthode appelée parse_qsl qui analyse les paramètres de requête URL en utilisant aussi bien le point-virgule que l’esperluette. Cette méthode est ensuite utilisée dans des frameworks comme Tornado pour analyser les paramètres de requête, ce qui peut ouvrir la voie à une chaîne d’exploitation par empoisonnement du cache Web.
Bottle (CVE-2020-28473), Tornado et Rack se sont révélés vulnérables. Prenons un exemple de ce vecteur d’attaque exploitant Bottle.
Un attaquant utilise q=cat comme paramètre pour une zone de recherche, puis le remplace par une autre valeur. Voici la requête et la réponse :
Imaginons maintenant qu’un utilisateur fasse une recherche sur « cat » alors que la réponse malveillante est toujours en cache :
L’attaquant a pu modifier une requête légitime en remplaçant le paramètre de recherche. En effet, le serveur voit ici trois paramètres : q, utm_content, puis un autre q. Il remplace la valeur du premier paramètre q par celle du dernier. Le proxy, quant à lui, considère la chaîne entière ?q=cat&utm_content=1;q=dog! comme la valeur de utm_content. La clé de cache ne contient donc que localhost?q=cat.
Pour corriger cette vulnérabilité, il convient d’utiliser uniquement l’esperluette (&) comme séparateur de paramètres de requête, sauf indication contraire du développeur. Werkzeug, par exemple, permet aux développeurs de définir des paramètres personnalisés et utilise l’esperluette par défaut. Les mainteneurs de Bottle ont choisi de corriger le problème en ne séparant pas les chaînes de requête au niveau de ;, à partir de la version 0.12.19. Le framework Rails s’est également révélé vulnérable à cette méthode (découverte par James Kettle et signalée à notre équipe avec l’aide de Jonathan Leitschuh), mais le problème n’était toujours pas corrigé au moment de la rédaction de cet article.
Vulnérabilités liées aux paramètres de corps dans les requêtes GET (« fat GET ») dans Flask et Tornado

Certains proxys, NGINX par exemple, permettent d’inclure des paramètres de corps dans une requête GET. Bien que cela ne soit pas strictement interdit par la RFC HTTP (« Le contenu d’un message de requête GET n’a pas de sémantique définie ; l’envoi d’un corps dans une requête GET peut amener certaines implémentations existantes à rejeter la requête »), plusieurs frameworks se sont révélés inclure ces paramètres dans des méthodes intégrées qui ne sont pas explicitement destinées aux paramètres de corps.
Les développeurs peuvent alors chercher à récupérer des paramètres de requête GET, mais récupérer à la place des paramètres de corps. Ces paramètres ne sont pas indexés dans le cache, ce qui peut entraîner deux problèmes :
Remplacement de paramètres
Un attaquant peut remplacer les paramètres de requête GET par des paramètres de corps GET et faire parvenir la réponse mise en cache à d’autres utilisateurs.
Ce problème a été détecté dans Tornado utilisé avec NGINX.
Comme Tornado donne la priorité aux paramètres de corps, il était possible de remplacer les requêtes d’utilisateurs sans méfiance par des requêtes malveillantes. Reprenons notre exemple précédent : une recherche sur « cat ».
Lorsqu’un utilisateur recherche « cat », le déroulement est alors le suivant :
Dans un scénario où il existe une vulnérabilité de cross-site scripting (XSS) réfléchi, cette technique peut la transformer en XSS stocké, qui peut ensuite toucher d’autres utilisateurs de l’application.
Injection de paramètres supplémentaires
Un attaquant peut injecter des paramètres supplémentaires et faire parvenir la réponse mise en cache à d’autres utilisateurs. Cette situation est moins grave que la première, mais peut néanmoins avoir des conséquences critiques si elle est combinée aux bons mécanismes. Par exemple, certaines implémentations permettent de modifier la méthode de requête à l’aide de _method. Cette attaque a été démontrée dans Flask.
Cette requête ferait en sorte que toutes les requêtes suivantes des utilisateurs sans méfiance contiennent le paramètre supplémentaire.
Snyk a constaté que plusieurs frameworks autorisaient ce comportement. Toutefois, plusieurs mainteneurs que nous avons contactés n’ont pas considéré qu’il s’agissait d’une vulnérabilité directe de leur package. Pour cette raison, Snyk a décidé de ne pas publier d’avis de sécurité concernant ces problèmes.
Il est toutefois possible de corriger le problème directement dans les packages en empêchant les développeurs de récupérer les données de corps avec ces méthodes ambiguës, car beaucoup utilisent request.params par commodité, sans en connaître les conséquences. Une autre solution consiste à ne pas donner la priorité aux paramètres de corps lorsque ces méthodes ambiguës sont utilisées, afin d’éviter qu’ils remplacent des paramètres de requête légitimes.
Par exemple, les mainteneurs de Werkzeug ont choisi d’empêcher request.values d’utiliser request.form dans les requêtes GET. Les mainteneurs de Tornado ont opté pour une autre approche : ajouter un indicateur pour que l’analyse des corps de requêtes GET soit activée uniquement sur demande (cette version n’est pas encore publiée).
Portée des mesures correctives
Au cours de cette étude, nous avons constaté que la complexité des différents vecteurs d’attaque empêchait parfois les mainteneurs de déterminer si les mesures correctives relevaient de leur bibliothèque ou devaient être mises en œuvre au niveau du proxy.
Il existe de nombreux scénarios susceptibles d’entraîner un empoisonnement du cache, et il n’incombe pas au framework Web de tous les atténuer. Cela dit, le framework Web peut contribuer à protéger contre certains d’entre eux en mettant en place des mesures de défense en profondeur supplémentaires.
On pourrait faire valoir que les proxys ne devraient pas ignorer les paramètres de corps des requêtes GET, puisque de nombreuses implémentations les utilisent encore. Ils peuvent toutefois mettre ces paramètres en cache comme s’il s’agissait de paramètres de requête. De leur côté, les frameworks peuvent empêcher les développeurs d’utiliser des méthodes ambiguës tout en autorisant les paramètres de corps. Il en va de même pour la dissimulation de paramètres : les proxys ne devraient pas utiliser le point-virgule comme séparateur, car la RFC ne le recommande pas, tandis que les frameworks peuvent ne l’autoriser que si le développeur le définit explicitement.
Réduire les risques d’empoisonnement du cache en tant que développeur
Chaque développeur peut réduire le risque de vulnérabilité en suivant ces recommandations :
Connaissez votre clé de cache : si votre serveur sépare les arguments de requête à l’aide d’un point-virgule, vérifiez que votre proxy de cache en fait autant. Assurez-vous également que votre clé de cache contient les en-têtes nécessaires pour empêcher les attaquants d’exploiter des paramètres non indexés afin d’empoisonner le cache Web.
Ignorez les paramètres de corps des requêtes GET, sauf s’ils sont nécessaires au fonctionnement du programme ; dans ce cas, veillez à ne les utiliser que lorsque c’est nécessaire.
Détectez et corrigez les autres vulnérabilités de votre application :Le web cache poisoning est généralement utilisé dans une chaîne d’exploitation, où un attaquant peut envoyer une réponse malveillante à d’autres utilisateurs, par exemple en transformant une XSS réfléchie en XSS stockée. Les développeurs doivent faire de leur mieux pour protéger leurs applications contre ces vulnérabilités courantes, même si elles semblent moins graves.
Résumé
En conclusion, cette étude montre que les frameworks open source sont vulnérables aux attaques de web cache poisoning presque quel que soit le proxy utilisé (à quelques exceptions près). S’il est possible d’atténuer ces attaques au niveau du proxy, de nombreux développeurs ne connaissent pas ces vecteurs d’attaque et ne mettent pas en place les mesures de protection nécessaires au niveau du cache et du proxy.
L’objectif de cet article de blog était de sensibiliser la communauté des développeurs. Bien que nous n’ayons présenté que deux vecteurs possibles de web cache poisoning, il en existe beaucoup d’autres. Les développeurs doivent essayer de suivre les recommandations ci-dessus et toujours garder à l’esprit ces vulnérabilités particulières.
Lancez-vous dans les compétitions Capture The Flag
Apprenez à résoudre des défis de capture du drapeau en regardant à la demande notre atelier virtuel d’initiation.
