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-storeValues and what each does
| Directive | Status | Description |
|---|---|---|
no-store | ✅ Good | The security directive. No cache, private or shared, may store any part of the request or response. |
private | ✅ Good | Shared caches must not store the response. The browser cache still can, so it is not a substitute for no-store. |
no-cache | ❌ Risky | Unsafe as a confidentiality control. It means revalidate before reuse, not do not store; the bytes still land on disk. |
public | ❌ Risky | Lets any cache store the response, including some it would otherwise refuse. Never on personalized responses. |
max-age=<seconds> | ✅ Good | Freshness lifetime, a performance control. max-age=0 marks the response stale immediately. |
s-maxage=<seconds> | ✅ Good | Freshness lifetime for shared caches only (CDN tuning); overrides max-age for them. Side effect below. |
must-revalidate | ✅ Good | A 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 minimumprivate) 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 resourcesno-storeandprivateis 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-cacheis notno-store. The classic confusion.no-cachemeans 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, sendno-store.privatestill 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 needno-store, notprivate.- The back/forward cache ignores your directives.
no-cacheandmust-revalidatehave never guaranteed revalidation on a history navigation, and since Chrome's 2025 rollout evenno-storepages 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 sendingClear-Site-Dataon logout. - The CDN can override you. CDNs honor the origin's
Cache-Controlby 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. PragmaandExpiresare legacy. RFC 9111 deprecatesPragma(it was historically request-only and was never specified for responses), andExpiresis ignored whenevermax-ageis present. Neither is needed for any modern browser.
How to set it up
-
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.
-
Send
no-storeon every sensitive response, from the server, per response:Cache-Control: no-store -
Send
private, no-cacheon personalized responses the browser may keep but shared caches must not touch:Cache-Control: private, no-cache -
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 -
On logout, invalidate the session server-side and send
Clear-Site-Dataso the browser drops what it stored:Clear-Site-Data: "cache","cookies","storage" -
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-storeSend 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
- Security headers overview
- Cookie security for the session cookies whose responses this header keeps out of caches
- Security headers scanner to check your caching directives and the rest of your deployed headers
Sources
Referrer-Policy
Referrer-Policy controls how much of the current URL leaks in the Referer header when users navigate away. Values, defaults, and the safe choice.
Cross-Origin-Resource-Policy
CORP lets a resource say who may embed it, protecting against cross-origin leaks and hotlinking. Values and how it pairs with COEP.