CentralCSP
Security headers

Cache-Control

The security side of Cache-Control, keeping responses with personal or session data out of shared caches and the browser cache with no-store.

Last update:

Cache-Control is the response header that decides whether a copy of the response may be stored and reused, by the browser's own private cache and by shared caches (CDNs, reverse proxies, corporate proxies) that serve one stored response to many users. Caches store and reuse by design: HTTP is built to cache as much as possible, so even a response with no caching headers at all can be stored and reused. This is called heuristic caching.

That default is the security problem. A personalized page (an account dashboard, an order confirmation) stored in a shared cache can be served to a different user. This page covers only that security side, keeping sensitive responses out of caches; performance tuning with this header is out of scope.

For a response carrying personal or session data:

Cache-Control: no-store

Values and what each does

DirectiveStatusDescription
no-store✅ GoodThe security directive. No cache, private or shared, may store any part of the request or response.
private✅ GoodShared caches must not store the response. The browser cache still can, so it is not a substitute for no-store.
no-cache❌ RiskyUnsafe as a confidentiality control. It means revalidate before reuse, not do not store; the bytes still land on disk.
public❌ RiskyLets any cache store the response, including some it would otherwise refuse. Never on personalized responses.
max-age=<seconds>✅ GoodFreshness lifetime, a performance control. max-age=0 marks the response stale immediately.
s-maxage=<seconds>✅ GoodFreshness lifetime for shared caches only (CDN tuning); overrides max-age for them. Side effect below.
must-revalidate✅ GoodA response that has gone stale must be revalidated before reuse. Side effect below.

Two defaults catch almost everyone out. First, Set-Cookie does not prevent caching: RFC 9111 states that a cacheable response with a Set-Cookie header can be, and often is, used to satisfy later requests. Second, heuristic caching means a response with no caching headers at all may still be stored, with the cache inventing its own freshness lifetime. Sending nothing is not a policy; it is permission.

One nuance on s-maxage and must-revalidate: shared caches must not store a response to a request that carried an Authorization header, unless public, s-maxage, or must-revalidate explicitly allows it. So those two directives quietly unlock shared caching of Authorization responses. And that built-in protection covers only the Authorization header, not cookie-based sessions, which is how most modern applications authenticate.

What it protects against

  • Other users reading personalized pages from a shared cache. This is not theoretical. On December 25, 2015, during a denial-of-service attack, a caching partner's configuration change incorrectly cached Steam web traffic for authenticated users, and around 34,000 users were served other users' Store pages, exposing billing addresses, email addresses, purchase histories, and the last digits of phone numbers and payment cards (SecurityWeek's coverage). no-store (or at minimum private) on those responses is what prevents a shared cache from ever holding them.
  • Post-logout reading on shared machines. On a library or office computer, the next person can press the back button and redisplay a cached account page after the previous user logged out. OWASP tests for exactly this (WSTG-ATHN-06, testing for browser cache weaknesses).
  • Web cache deception. An attacker tricks a cache into storing a sensitive dynamic page through path tricks like /account/settings.css; marking dynamic resources no-store and private is the PortSwigger-documented defense.
  • Web cache poisoning. The integrity counterpart: an attacker gets a harmful response stored and served to other users (PortSwigger on web cache poisoning). Strict caching directives shrink what a poisoned cache can hold.

Risks without it

Without the header, sensitive responses are storable by default. Heuristic caching lets caches store and reuse responses that carry no caching headers, and Set-Cookie does not inhibit that, so "it sets a session cookie" is no protection at all. A personalized page served with no directives can sit in the browser cache of a shared machine and, worse, in a shared cache where it can answer another user's request.

That is exactly what the security headers scanner means by a "sensitive response cacheable" finding: a response that sets or requires a session cookie, with no directive (no-store or private) preventing shared-cache storage. The fix is always to send the directive from the server; no cache will infer it for you.

Risks and gotchas when using it

  • no-cache is not no-store. The classic confusion. no-cache means the cache may store the response but must revalidate it before reuse. As a confidentiality control it does nothing: the bytes are still on disk. When you mean do not store, send no-store.
  • private still lets the browser store. It only excludes shared caches. On a shared machine the page is still in the profile's disk cache, so sensitive responses need no-store, not private.
  • The back/forward cache ignores your directives. no-cache and must-revalidate have never guaranteed revalidation on a history navigation, and since Chrome's 2025 rollout even no-store pages can be restored from the back/forward cache (with mitigations: the entry is evicted when cookies change, and it lives at most 3 minutes). The consequence: logout must actually invalidate the session, not just navigate away, and the OWASP Session Management cheat sheet recommends sending Clear-Site-Data on logout.
  • The CDN can override you. CDNs honor the origin's Cache-Control by default, but edge configuration (for example Cloudflare's cache rules) can override it, which is how the Steam incident happened. Audit the CDN's cache rules along with the header.
  • Pragma and Expires are legacy. RFC 9111 deprecates Pragma (it was historically request-only and was never specified for responses), and Expires is ignored whenever max-age is present. Neither is needed for any modern browser.

How to set it up

  1. Classify your responses into three groups: sensitive (anything with session or personal data), personalized but safe for the browser to cache, and static fingerprinted assets.

  2. Send no-store on every sensitive response, from the server, per response:

    Cache-Control: no-store
  3. Send private, no-cache on personalized responses the browser may keep but shared caches must not touch:

    Cache-Control: private, no-cache
  4. Keep long freshness on static assets with a hash or version in the filename; they are the same bytes for every user:

    Cache-Control: max-age=31536000, immutable
  5. On logout, invalidate the session server-side and send Clear-Site-Data so the browser drops what it stored:

    Clear-Site-Data: "cache","cookies","storage"
  6. Verify the deployed headers with the security headers scanner, and check that no CDN cache rule overrides what the origin sends.

Recommendation

Send Cache-Control: no-store on every response that carries session or personal data:

Cache-Control: no-store

Send no-store on any response containing session identifiers or personal data, the recommendation in the OWASP Session Management cheat sheet and WSTG-ATHN-06; with no-store in place, extra directives and Expires are generally unnecessary for modern browsers. Apply it per response, never site-wide: sensitive pages get no-store, personalized pages get private, no-cache, and fingerprinted static assets keep their long max-age.

Browser support

Baseline widely available: the response directives on this page work in every current browser, proxy, and CDN, so support is effectively universal. The thing to verify is not the browser but the edge: a CDN honors your header by default yet can be configured to override it, so the deployed behavior is only as good as the cache rules in front of the origin.

FAQ

What is the difference between no-cache and no-store?

no-store tells caches to keep no copy of the response at all. no-cache allows a copy to be stored but requires revalidation with the origin before any reuse, so the bytes still sit on disk. When you mean do not store a sensitive response, use no-store. See no-cache vs no-store for the full comparison.

Should login pages be cached?

No. Login, account, and any response that carries or requires a session should send Cache-Control: no-store, following the OWASP session management guidance. Without it a shared cache can store the personalized page and serve it to a different user, and the back button can redisplay it after logout.

Does Cache-Control private stop the browser caching?

No. private only tells shared caches, such as CDNs and corporate proxies, not to store the response; the browser's own cache may still keep it. To prevent storage everywhere, including on disk in the browser, use no-store instead. private narrows the audience, it does not stop caching.

See also

Sources

On this page