Attaque cache deception CDN et contre-mesures
Section intitulée « Attaque cache deception CDN et contre-mesures »La cache deception exploite un désaccord entre le CDN (Content Delivery Network), qui décide quoi mettre en cache, et l’application d’origine, qui décide quoi servir. Une page authentifiée peut alors être stockée comme un fichier statique et devenir accessible à tout visiteur demandant la même adresse.
Cette attaque ressemble à un guichet qui classe par erreur un relevé bancaire dans le rayon des prospectus gratuits. Le relevé n’a pas changé de contenu, mais il devient disponible au mauvais endroit, pour les mauvaises personnes, jusqu’à ce que quelqu’un remarque le classement absurde.
→ Lire l’explication complète de la marmotte
Mécanisme de cache deception entre CDN et origine
Section intitulée « Mécanisme de cache deception entre CDN et origine »Un CDN réduit la charge de l’origine en conservant des réponses réutilisables. Pour décider si une réponse mérite ce traitement, il examine généralement l’URL (Uniform Resource Locator), son extension, les en-têtes HTTP (Hypertext Transfer Protocol), les règles de routage et la politique de cache renvoyée par l’origine.
L’attaque apparaît lorsque le CDN considère /compte/profil.css comme une ressource statique, tandis que l’application traite cette même route comme /compte/profil. L’origine renvoie alors une page personnelle, car le navigateur porte un cookie de session valide. Le CDN stocke cette réponse sous une clé associée à l’URL finissant par .css. Un second visiteur demande cette URL et reçoit la réponse mise en cache, sans session.
Le défaut n’est pas qu’un cache existe. Le défaut est que deux composants ne parlent pas de la même ressource. L’un classe selon une terminaison ou une expression régulière, l’autre selon son routeur applicatif. Les deux ont raison dans leur logique locale. Ensemble, ils fabriquent une fuite de données avec la régularité d’un tableur de gouvernance oublié dans un partage public.
Les variantes exploitables dépendent des écarts de normalisation :
- suffixes statiques ajoutés à une route dynamique :
/profil/avatar.jpg; - paramètres de chemin, par exemple
/profil;avatar.css; - encodages divergents de
/,.,%2fou%3b; - suppression des paramètres de requête par le CDN ;
- règles qui ignorent le chemin après une extension connue ;
- réécriture côté origine qui retire un suffixe ou redirige vers une route authentifiée.
Une cache deception n’est pas une cache poisoning. La cache poisoning injecte une réponse choisie par l’attaquant pour empoisonner le cache. Ici, l’attaquant fait stocker une réponse légitime, mais privée, générée pour une victime. Cette distinction compte : filtrer les entrées douteuses ne corrige pas un cache qui accepte de publier une réponse authentifiée.
Le compromis central est net. Mettre agressivement en cache les routes ambiguës augmente le taux de réponse servi depuis le CDN et réduit la charge d’origine. En échange, la frontière entre contenu public et contenu privé devient une convention répartie entre plusieurs règles. Cette convention casse tôt ou tard. La recommandation est donc de refuser le cache partagé par défaut pour toute route pouvant être authentifiée, puis d’autoriser explicitement les seuls chemins statiques publics. Le gain marginal de cache ne vaut pas l’exposition d’un profil, d’une facture ou d’un jeton anti-forgery.
Préconditions d’exploitation et surface d’attaque
Section intitulée « Préconditions d’exploitation et surface d’attaque »L’attaque exige quatre conditions cumulatives :
- une route d’origine renvoie du contenu lié à une session ;
- le CDN accepte de mettre cette route en cache ;
- la clé de cache ne varie pas selon l’identité de session ;
- l’attaquant peut faire demander à une victime une URL qui prend une forme statique pour le CDN.
Le cookie de session est souvent absent de la clé de cache, ce qui est normal pour le contenu public. L’inclure systématiquement dans la clé ne constitue pas une correction de référence : cela crée un cache par utilisateur, fait exploser la cardinalité et peut conserver des réponses privées plus longtemps que prévu. Un cache privé mal purgé reste privé, mais il reste aussi un passager clandestin coûteux.
Les en-têtes Cache-Control: private, no-store doivent interdire le stockage partagé. Ils ne protègent rien si le CDN les écrase, les ignore dans une règle prioritaire ou applique une durée de vie forcée. La présence de Set-Cookie doit également empêcher le stockage partagé, sauf exception documentée et testée. Mettre public, max-age=3600 sur une réponse qui contient un identifiant de compte n’est pas de l’optimisation. C’est une publication différée avec une police de caractères plus petite.
Les routes de téléchargement sont particulièrement exposées. Une application peut autoriser /documents/releve.pdf, mais une couche intermédiaire peut classifier toute extension .pdf comme publique. Les réponses JSON (JavaScript Object Notation) ne sont pas immunisées : une règle de cache basée sur .json, /api/ ou un type MIME (Multipurpose Internet Mail Extensions) mal interprété suffit.
La revue doit couvrir le parcours complet, du navigateur à l’origine : pare-feu applicatif, répartiteur de charge, CDN, proxy inverse, serveur web, routeur applicatif. La page suivre un flux de bout en bout rappelle pourquoi un paquet observé à une seule étape raconte souvent une histoire incomplète. Le cache est une machine à mémoriser les erreurs de classification. Il le fait sans méchanceté, ce qui est presque vexant.
Démonstration en laboratoire avec proxy cache
Section intitulée « Démonstration en laboratoire avec proxy cache »Ce laboratoire reproduit volontairement une configuration dangereuse. Il ne doit pas être exposé sur Internet ni raccordé à une application contenant des données réelles. L’objectif est de voir la fuite, puis de vérifier que la correction l’empêche.
Créer app.py :
# application d'origine volontairement permissive pour le laboratoirefrom flask import Flask, request, make_response
app = Flask(__name__)
@app.get("/compte/profil.css")def profile(): # Le cookie représente l'identité de la victime authentifiée. user = request.cookies.get("session", "anonyme") response = make_response(f"profil prive de {user}\n") # L'origine demande explicitement de ne pas partager cette réponse. response.headers["Cache-Control"] = "private, no-store" return responseCréer ensuite la configuration Nginx suivante dans nginx.conf :
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=lab:10m inactive=10m;
server { listen 8080;
location ~ \.css$ { proxy_pass http://app:5000; proxy_cache lab;
# Vulnérabilité volontaire : suppression de la politique de l'origine. proxy_hide_header Cache-Control; add_header Cache-Control "public, max-age=300";
# La clé exclut le cookie de session : toute victime partage l'objet. proxy_cache_key "$scheme$proxy_host$request_uri"; add_header X-Cache-Status $upstream_cache_status always; }}Le fichier docker-compose.yml peut lancer nginx:alpine et une image Python contenant Flask, avec le service applicatif nommé app. Après démarrage, exécuter les requêtes suivantes :
# Première requête : la victime alimente le cache avec son profil.curl -i -H 'Cookie: session=alice' http://localhost:8080/compte/profil.css
# Seconde requête : aucun cookie, mais la réponse stockée est renvoyée.curl -i http://localhost:8080/compte/profil.cssLa seconde réponse doit contenir profil prive de alice et l’en-tête X-Cache-Status: HIT. Sans ce HIT, aucune démonstration n’existe : l’origine a peut-être répondu deux fois et le scénario raconte autre chose.
Corriger le laboratoire en supprimant proxy_hide_header, en retirant add_header Cache-Control "public, max-age=300" et en désactivant proxy_cache sur toute route authentifiée. Répéter les deux requêtes : la seconde doit produire profil prive de anonyme, ou une réponse 401 Unauthorized si l’application exige une session. Toute autre sortie mérite une analyse avant de déclarer la faille close.
Contre-mesures de routage et politique de cache
Section intitulée « Contre-mesures de routage et politique de cache »La mesure la plus fiable consiste à séparer les espaces de noms. Les fichiers publics doivent vivre sous un préfixe dédié, par exemple /assets/, ou sur un hôte distinct tel que static.example.net. Les routes applicatives ne doivent jamais accepter arbitrairement des suffixes de fichiers. Une ressource dynamique appelée /compte/profil.css est une dette de routage, même si l’application y répond avec le sourire.
Le CDN doit appliquer une liste d’autorisation de chemins cacheables. Une règle du type « mettre en cache tout ce qui finit par .css, .js, .png ou .pdf » est insuffisante si ces extensions peuvent atteindre le routeur applicatif. Limiter cette règle à /assets/ réduit radicalement la surface d’attaque. Les extensions ne sont pas une frontière de sécurité, seulement une habitude de développeur avec une longue carrière de malentendus.
Côté origine, chaque réponse authentifiée doit porter Cache-Control: private, no-store, ou au minimum private. no-store est préférable pour les pages contenant des données personnelles, des secrets de récupération, des codes à usage unique ou des documents temporaires. Ajouter Vary: Cookie ne remplace pas cette politique : cet en-tête modifie la variation du cache, il ne transforme pas une réponse privée en réponse publiable sans risque.
Les proxys doivent rejeter ou normaliser identiquement les chemins ambigus : doubles séparateurs, segments . et .., encodages réservés, paramètres de matrice et suffixes après une route dynamique. Cette normalisation doit être définie une fois, testée aux deux extrémités et journalisée. Faire confiance à la normalisation implicite de chaque produit revient à organiser une réunion entre cinq interprètes sans langue commune.
Une revue de cette classe de défauts complète l’inventaire présenté dans la section failles. Le test ne doit pas se limiter aux URL documentées : générer des variantes de chemins, avec et sans session, puis comparer statut, corps, en-têtes Age, Cache-Control et indicateur de cache.
Exploitation, supervision et reprise après fuite
Section intitulée « Exploitation, supervision et reprise après fuite »Un cache partagé ne doit jamais stocker une réponse qui transporte Set-Cookie, Authorization, un marqueur de session ou une donnée dépendante d’identité. Cette règle doit être contrôlée par test automatisé au déploiement : requête authentifiée, puis requête anonyme sur la même URL et ses variantes statiques. Le test doit échouer si le corps ou un identifiant utilisateur réapparaît.
La supervision doit enregistrer la clé de cache logique, le statut HIT ou MISS, la politique de cache reçue de l’origine et celle appliquée en sortie. Ne jamais journaliser le cookie brut, les en-têtes Authorization ou le contenu de la réponse. Un identifiant de corrélation pseudonymisé suffit à relier les événements. Les journaux qui contiennent déjà la fuite ne doivent pas devenir la deuxième fuite, ce serait du théâtre expérimental.
La purge d’un objet compromis doit viser toutes les variantes connues : chemin normalisé, requêtes avec paramètres, alias d’hôte et objets dérivés. Purger uniquement l’URL rapportée par l’alerte laisse souvent la variante encodée disponible. Le délai de propagation de purge doit être mesuré, pas supposé.
La rotation des jetons d’interface d’administration du CDN doit suivre une durée courte et des droits minimaux. Un jeton capable de purger, modifier les règles et lire les journaux ne doit pas être distribué à chaque pipeline comme des bonbons de conférence. Les configurations de cache doivent être versionnées, sauvegardées et restaurables ; la reprise impose aussi de réappliquer les politiques de non-cache avant de réactiver le trafic.
Enfin, purger ne révoque pas ce qui a déjà été lu. Si la réponse divulguée contient une session, un lien signé ou un code de récupération, il faut révoquer cet artefact à la source. Le CDN sait oublier une copie. Il ne sait pas remonter le temps, malgré les promesses parfois très PowerPoint.
🐝 Ce qui arrive si on s’en passe
Section intitulée « 🐝 Ce qui arrive si on s’en passe »Sans ces contre-mesures, la lecture de la copie divulguée ne laisse aucune trace côté application : la requête anonyme reçoit un HIT servi par le cache et n’atteint jamais l’origine. Les journaux applicatifs ne montrent que la requête légitime de la victime, les contrôles d’authentification forte, les limites de débit et la détection d’anomalies sur les sessions ne se déclenchent pas, puisque personne ne s’authentifie. Pendant toute la durée de vie de l’objet, un nombre indéterminé de visiteurs peut relire la même réponse, et seuls les journaux du cache permettent d’estimer qui l’a obtenue. Si ces journaux n’enregistrent pas la clé de cache et le statut HIT, le périmètre de la fuite (quels comptes, combien de lectures, depuis quand) ne peut plus être établi a posteriori.
🦫 Explication de la marmotte 🍫
Section intitulée « 🦫 Explication de la marmotte 🍫 »Un service placé entre un site et ses visiteurs peut garder des copies de certaines pages pour répondre plus vite. Il doit donc décider quelles pages sont publiques et lesquelles dépendent de la personne connectée. Une attaque de désynchronisation arrive quand ce service et le site ne prennent pas la même décision pour une même adresse.
Une victime connectée ouvre une adresse fabriquée pour ressembler à celle d’un fichier public, par exemple avec une fin en .css. Le site reconnaît pourtant cette adresse comme une page de profil et renvoie les données privées de la victime. Le service intermédiaire croit, lui, avoir reçu un fichier public. Il garde alors la réponse et la rend disponible à toute personne qui demande exactement cette adresse.
L’attaque fonctionne seulement si la réponse privée est conservée sans tenir compte de l’identité de la personne. Elle échoue si le site interdit le stockage de ses pages privées, si le service respecte cette interdiction et si seules des adresses réservées aux fichiers publics peuvent être conservées.
La correction consiste à séparer les adresses des fichiers publics et celles des pages personnelles, à refuser le stockage des réponses liées à une connexion et à tester les variantes d’adresses inhabituelles. Si une fuite est découverte, effacer les copies ne suffit pas toujours : les liens temporaires, codes ou accès présents dans la page divulguée doivent aussi être annulés.
À lire aussi
- Mettre en place un rate limiting anti-DDoS L7Guide pas-à-pas pour mesurer, configurer et tester un rate limiting applicatif contre un DDoS de couche 7.
- Cross-site scripting (XSS) : mécanisme, variantes et défensesComprendre le cross-site scripting (XSS), ses variantes réfléchie, stockée, DOM et par mutation, et les défenses qui tiennent réellement en production.
- Comment empêcher une attaque de fixation de sessionDémonstration en laboratoire de l’attaque de fixation de session et contre-mesure par régénération de l’identifiant après authentification.
- Comment bloquer un webhook forgé sans signature vérifiéeDémonstration d’une attaque de webhook non vérifié, de son mécanisme en laboratoire et des contre-mesures de signature, anti-rejeu et exploitation.
Pages voisines
La citent
- Cross-site scripting (XSS) : mécanisme, variantes et défenses
- Performance, QoS, capacité et sobriété énergétique
Attaque cache deception CDN et contre-mesures
Elle cite
: les deux pages se citent l’une l’autre.
