Back

Ce Qu'il Ne Faut Jamais Mettre en Cache

Ce Qu'il Ne Faut Jamais Mettre en Cache

La mise en cache est l’un des meilleurs outils pour optimiser les performances frontend. Bien utilisée, elle élimine les requêtes réseau redondantes, réduit la charge serveur et donne à votre application une réactivité quasi instantanée. Mal utilisée, elle expose des données privées, sert du contenu authentifié périmé ou enferme les utilisateurs dans des états de page défaillants dont ils ne peuvent pas s’échapper.

La règle est plus simple que la plupart des articles ne le laissent entendre : les ressources statiques, versionnées et publiques peuvent presque toujours être mises en cache de manière agressive. Tout le reste doit être considéré comme non cacheable par défaut.

Avant d’aborder ce qu’il ne faut pas mettre en cache, il est utile de préciser la mise en cache se produit, car ces couches se comportent de manière très différente.

Points Clés

  • Les couches de cache (HTTP, CDN, Service Worker, bfcache, Web Storage) ont des portées et des caractéristiques de sécurité distinctes ; les traiter de manière interchangeable génère de véritables bugs.
  • no-store empêche tout stockage, tandis que no-cache force uniquement la revalidation avant réutilisation.
  • Les réponses authentifiées ou spécifiques à un utilisateur doivent utiliser Cache-Control: private, no-store pour empêcher les caches partagés de faire fuiter des données entre utilisateurs.
  • Ne stockez jamais des JWT, des refresh tokens ou des clés d’API dans localStorage ou IndexedDB ; préférez les cookies HttpOnly.
  • Utilisez no-store par défaut dès qu’une copie périmée ou partagée pourrait poser un problème de sécurité ou d’exactitude.

Les Couches de Cache Ne Sont Pas Interchangeables

La plupart des erreurs de mise en cache commencent par traiter ces éléments comme identiques :

  • Cache HTTP/navigateur — Contrôlé par les en-têtes Cache-Control conformément à la RFC 9111. Partagé entre les onglets, persiste entre les sessions.
  • Cache CDN/partagé — S’intercale entre vos utilisateurs et votre serveur d’origine. Les services comme Cloudflare CDN mettent en cache les réponses pour tous les utilisateurs, pas seulement pour un seul.
  • Service Worker Cache API — Un cache programmable que vous contrôlez entièrement en JavaScript. Persiste jusqu’à ce qu’il soit explicitement vidé.
  • bfcache (cache arrière/avant) — L’instantané en mémoire d’une page complète réalisé par le navigateur, utilisé lors de la navigation en arrière. Distinct du cache HTTP.
  • localStorage / sessionStorage — Stockage clé-valeur. Pas de gestion d’expiration, entièrement accessible en JavaScript.
  • IndexedDB — Stockage persistant structuré. Même surface d’exposition aux attaques XSS que localStorage.

Chacune de ces couches possède des caractéristiques différentes en termes de persistance, de portée et de sécurité. Les confondre engendre de véritables bugs.

Précisions sur la Sémantique de Cache-Control

Cette distinction est souvent mal rapportée ; il convient donc de l’énoncer clairement :

  • no-store signifie que le cache ne doit pas stocker la réponse du tout.
  • no-cache ne signifie pas « ne pas mettre en cache ». Cela signifie que la copie en cache doit être revalidée auprès du serveur avant toute réutilisation.

Si vous souhaitez qu’une réponse ne soit jamais stockée nulle part, utilisez no-store. Si vous voulez garantir la fraîcheur à chaque utilisation tout en conservant l’efficacité des requêtes conditionnelles (ETags, 304), utilisez no-cache.

Ce Qu’il Ne Faut Jamais Mettre en Cache

Les Réponses d’API Authentifiées et le HTML Spécifique à un Utilisateur

Toute réponse qui varie selon l’identité de l’utilisateur — HTML de tableau de bord, pages de compte, réponses d’API contenant des données de profil — ne doit pas être stockée dans un cache partagé. Un schéma courant est :

Cache-Control: private, no-store

no-store empêche les caches HTTP normaux de stocker la réponse, tandis que private indique explicitement aux caches partagés tels que les CDN de ne pas la mettre en cache pour plusieurs utilisateurs.

Sans la directive private, un CDN comme Cloudflare peut mettre en cache une réponse retournée pour l’Utilisateur A et la servir à l’Utilisateur B. Ce comportement a provoqué de véritables incidents d’exposition de données.

Tout Ce Qui, dans un Service Worker, Requiert une Authentification

La Service Worker Cache API est en pratique un proxy réseau programmable. Un service worker intercepte chaque requête fetch dans sa portée. Si vous mettez en cache aveuglément des réponses d’API authentifiées ou du HTML spécifique à un utilisateur, ces données persistent dans le stockage de la Cache API — potentiellement entre les sessions, et accessibles au service worker quel que soit l’état de connexion.

// ❌ Ne faites pas cela
self.addEventListener('fetch', event => {
  event.respondWith(
    fetch(event.request).then(response => {
      return caches.open('v1').then(cache => {
        cache.put(event.request, response.clone());
        return response;
      });
    })
  );
});

Ce schéma met tout en cache de manière indiscriminée. Excluez explicitement les routes authentifiées :

// ✅ Ne mettez en cache que les ressources publiques et versionnées
const CACHEABLE = ['/shell.html', '/app-abc123.js', '/styles-f9c.css'];

self.addEventListener('fetch', event => {
  const url = new URL(event.request.url);
  if (url.origin !== self.location.origin) return; // ignorer les requêtes cross-origin
  if (!CACHEABLE.includes(url.pathname)) return;   // laisser passer vers le réseau
  event.respondWith(
    caches.match(event.request).then(r => r || fetch(event.request))
  );
});

Les Secrets dans localStorage ou IndexedDB

L’OWASP HTML5 Security Cheat Sheet est explicite à ce sujet : stocker des JWT, des refresh tokens, des clés d’API ou des tokens de réinitialisation de mot de passe dans localStorage ou IndexedDB est risqué, car toute vulnérabilité XSS sur votre domaine permet de les lire. Ces mécanismes de stockage n’ont pas de gestion d’expiration intégrée, pas d’équivalent à HttpOnly, et aucune isolation vis-à-vis des scripts injectés.

Utilisez des cookies HttpOnly, Secure, SameSite pour les tokens dans la mesure du possible. Si vous devez utiliser localStorage, sachez que vous acceptez ce risque en connaissance de cause.

Les Réponses Spécifiques à un Utilisateur au Niveau du CDN

Une erreur courante avec Cloudflare CDN : oublier la directive private sur les réponses contenant des données utilisateur, ou mal configurer Vary.

# ❌ Directive private manquante — le CDN peut mettre ceci en cache pour tous les utilisateurs
Cache-Control: max-age=300
Content-Type: application/json

# ✅ Correct
Cache-Control: private, no-store

Si votre réponse varie légitimement selon un en-tête de requête (comme Accept-Language ou Authorization), vous devez inclure un en-tête Vary correctement configuré. Sans cela, un CDN peut servir une réponse en cache destinée à un utilisateur francophone à un utilisateur anglophone, ou pire encore, une réponse authentifiée en cache à un utilisateur non authentifié.

Soyez également vigilant face à l’empoisonnement de cache via des paramètres non inclus dans la clé de cache : si votre CDN met en cache selon l’URL mais que votre application lit un paramètre de requête ou un en-tête non validé pour construire la réponse, un attaquant peut empoisonner le cache avec une requête forgée.

Les Pages Qui Ne Devraient Pas Survivre au bfcache

Les navigateurs basés sur Chromium ont, dans leurs versions récentes, tendance à utiliser davantage le bfcache, même sur les pages qui envoient Cache-Control: no-store. Ce comportement dépend du navigateur et n’est pas universellement cohérent entre Chrome, Firefox et Safari. caniuse indique une prise en charge large du bfcache par les navigateurs modernes, mais l’interaction avec no-store diffère selon les moteurs et les versions de navigateur.

Si vous avez des pages pour lesquelles la restauration d’un instantané en mémoire périmé est véritablement dangereuse — comme une page affichant une confirmation de paiement unique ou un état sensible à la session — utilisez l’événement pageshow pour détecter les restaurations depuis le bfcache et recharger ou revalider la page :

window.addEventListener('pageshow', event => {
  if (event.persisted) {
    // La page a été restaurée depuis le bfcache
    window.location.reload();
  }
});

Ne supposez pas que no-store désactive universellement le bfcache sur tous les navigateurs.

Le Modèle Mental par Défaut

Si vous n’êtes pas sûr qu’un élément doive être mis en cache, posez-vous la question suivante : servir une copie périmée ou partagée de cet élément pourrait-il causer un problème de sécurité ou d’exactitude ? Si oui, utilisez Cache-Control: no-store par défaut et raisonnez à rebours à partir de là.

Les ressources statiques avec des URL dont le contenu est haché — vos bundles JavaScript, fichiers CSS, images — peuvent être mises en cache pendant un an avec max-age=31536000, immutable. Tout ce qui est lié à une session utilisateur, à un état d’authentification ou à une opération sensible ne le peut pas.

Conclusion

Une mise en cache agressive est une fonctionnalité. La mise en cache accidentelle des mauvais éléments est une vulnérabilité. La rigueur consiste à savoir quelle couche vous ciblez, ce que chaque directive Cache-Control garantit réellement, et quelles catégories de données ne doivent jamais quitter le contrôle strict du serveur d’origine. Traitez tout ce qui est lié à un utilisateur, une session ou un secret comme non cacheable par défaut, et réservez la mise en cache agressive aux ressources statiques et versionnées pour lesquelles elle a été conçue.

FAQ

C'est possible, mais non recommandé. Toute vulnérabilité XSS sur votre domaine donne à un attaquant un accès complet en lecture à localStorage, ce qui signifie qu'il peut voler le token et usurper l'identité de l'utilisateur. Les cookies HttpOnly, Secure, SameSite sont plus sûrs car JavaScript ne peut pas les lire directement. Si vous devez utiliser localStorage, vous acceptez le risque XSS et devez investir massivement dans la CSP et la désinfection des entrées.

no-store interdit aux caches HTTP normaux de stocker la réponse pour réutilisation. no-cache autorise le stockage de la réponse, mais exige une revalidation auprès du serveur d'origine avant chaque réutilisation, généralement via des requêtes conditionnelles ETag ou Last-Modified. Utilisez no-store pour les données sensibles et no-cache lorsque vous souhaitez des garanties de fraîcheur tout en bénéficiant des réponses 304 Not Modified.

Généralement parce que la réponse manque de la directive private ou possède un en-tête Vary incorrect. Sans private, un cache partagé traite la réponse comme cacheable pour tout le monde. Sans Vary sur des en-têtes comme Authorization ou Cookie, le CDN ignore ces en-têtes lors de la génération des clés de cache et peut servir la réponse de l'Utilisateur A à l'Utilisateur B. Envoyez toujours Cache-Control: private, no-store sur les réponses spécifiques à un utilisateur.

Il n'existe pas d'en-tête fiable et compatible avec tous les navigateurs pour désactiver le bfcache. Cache-Control: no-store fonctionne dans certains navigateurs, mais pas de manière cohérente. L'approche fiable consiste à écouter l'événement pageshow et à vérifier event.persisted. Si la valeur est true, la page a été restaurée depuis le bfcache et vous pouvez forcer un rechargement ou récupérer à nouveau l'état sensible. Cela est indispensable pour les pages de confirmation de paiement et les pages post-déconnexion.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay