Cache-Control and Clear-Site-Data for Sensitive Pages
This guide is part of the Web Security Headers Fundamentals reference and treats Cache-Control and Clear-Site-Data as the two controls that govern where authenticated data is allowed to persist — in intermediary caches, on disk, in the back/forward cache, and in client-side storage after a session ends. The two operational decisions that carry the most blast radius are which routes get no-store and what to purge on logout. The first is dissected in Cache-Control: no-store for sensitive pages; the second in using Clear-Site-Data on logout.
Threat Model & Protocol Mechanics
The threat these headers neutralize is not interception in transit — that is the job of HSTS — but persistence at rest on the client and in shared infrastructure. An account summary, a bank statement, a medical record, or a page echoing a session token is a sensitive object the moment it reaches the browser. If that object is allowed to be stored, it outlives the request that produced it, and the attacker no longer needs the network.
Four concrete leak paths matter:
- Shared/intermediary caches. A response without explicit cache directives may be stored by a forward proxy, a corporate gateway, or a misconfigured CDN. A response marked
publicor one that a heuristic-caching proxy treats as cacheable can be served to a different user behind the same proxy. The default for an authenticated response must never be “implicitly cacheable.” - On-disk browser cache. A logged-in user on a shared or kiosk machine walks away; the next person opens the browser cache directory, or simply presses the back button, and the rendered account page is still there. Disk cache survives the tab, the session, and frequently the logout.
- Back/forward cache (BFCache). Modern browsers freeze a fully live snapshot of the page — DOM, JavaScript heap, in-memory state — so the back button restores it instantly. After a user logs out and presses Back, BFCache can restore the authenticated page from memory, session intact-looking, even though the server has destroyed the session. BFCache is governed differently from the HTTP disk cache;
Cache-Control: no-storeis the directive browsers honor to keep a page out of BFCache. - Residual client state after logout. Cookies,
localStorage,sessionStorage, IndexedDB, and Service Worker caches all persist independently of the session. Destroying the server-side session does nothing to them. On a shared device, the next user inherits whatever the previous user left behind.
Cache-Control is defined by RFC 9111 (HTTP Caching, which obsoletes RFC 7234). Its security-relevant response directives are:
no-store— the strongest. No cache, shared or private, may store any part of the request or response. This is the directive that also keeps a page out of BFCache. Use it for any response containing data that must not survive the request.no-cache— a misleading name. The response may be stored, but it must be revalidated with the origin (a conditional request) before being reused. It does not prevent storage; it prevents stale reuse. It does not reliably keep a page out of BFCache.private— the response may be stored by the user’s own browser but not by any shared/intermediary cache. This protects against cross-user leakage in a proxy while still allowing the personal browser to cache. It is not sufficient on its own for a shared device.
Clear-Site-Data is defined by the W3C Clear-Site-Data specification. It is a response header whose value is a comma-separated list of quoted data types the browser must purge for the response’s origin:
"cache"— clears the network cache (HTTP cache entries) for the origin."cookies"— clears cookies and HTTP authentication credentials for the origin (and, per spec, registrable-domain-scoped cookies)."storage"— clears DOM storage:localStorage,sessionStorage, IndexedDB, Web SQL, Cache Storage, and Service Worker registrations for the origin."*"— a wildcard that clears all currently defined data types. Critically, the spec also defines"executionContexts"(reload/navigate live contexts of the origin), and"*"is intended to include it, so"*"can force-reload open tabs of the origin. Treat"*"as “purge everything and disrupt live tabs,” not a tidy synonym for the three named types.
The browser only honors Clear-Site-Data over a secure (HTTPS) transport; it is ignored on plaintext. Support is good in Chromium and Firefox; Safari’s support is partial and lagging — never rely on it as your only logout cleanup mechanism. The decision logic for selecting directives per page sensitivity is below.
The two headers are complementary, not interchangeable. Cache-Control governs storage during the authenticated session; Clear-Site-Data purges what already accumulated at the end of it. A complete posture sets no-store on sensitive responses and emits Clear-Site-Data on logout. Neither replaces transport encryption (HSTS) or content restriction (Content Security Policy).
Directive Syntax & Spec
Cache-Control: no-store
Cache-Control: private, no-cache, must-revalidate
Clear-Site-Data: "cache", "cookies", "storage"
Cache-Control directives are comma-separated, case-insensitive, and order-independent. Clear-Site-Data types are comma-separated quoted strings — the double quotes are part of the wire syntax and the header is ignored if they are omitted.
| Directive | Type | Default | Security Impact | When to Deviate |
|---|---|---|---|---|
Cache-Control: no-store |
Response flag | off | No cache stores the response; also keeps the page out of BFCache. The only directive that prevents on-disk persistence. | Apply only to sensitive routes; site-wide it destroys cacheability and CDN offload |
Cache-Control: no-cache |
Response flag | off | Response may be stored but must be revalidated before reuse. Does not prevent storage or BFCache. | Use for resources that change often but are safe to cache when fresh |
Cache-Control: private |
Response flag | off | Browser may cache; shared/intermediary caches may not. Stops cross-user leakage in proxies. | Use for per-user pages that are safe on the user’s own disk |
Cache-Control: must-revalidate |
Response flag | off | Forbids serving a stale cached copy after expiry without revalidation. | Pair with private/max-age=0 to prevent stale authenticated views |
Clear-Site-Data: "cache" |
Quoted token | n/a | Purges the origin’s HTTP cache. | Logout, or after a security event |
Clear-Site-Data: "cookies" |
Quoted token | n/a | Purges cookies and HTTP auth credentials for the origin/registrable domain. | Logout; primary token for session teardown |
Clear-Site-Data: "storage" |
Quoted token | n/a | Purges localStorage, sessionStorage, IndexedDB, Cache Storage, Service Workers. | Logout when client state holds user data |
Clear-Site-Data: "*" |
Quoted token | n/a | Purges all defined types including executionContexts — can force-reload open tabs. | Full account wipe; avoid when other tabs must stay live |
Common malformed-syntax gotchas:
- Unquoted Clear-Site-Data values (
Clear-Site-Data: cookies) — the spec requires quoted strings; the header is silently ignored. Pragma: no-cacheas the only directive —Pragmais an HTTP/1.0 request-header artifact. Browsers do not treat aPragmaresponse header as authoritative. Send realCache-Controldirectives;Pragmais at best a legacy belt-and-braces note.- Relying on
no-cacheto keep a page off disk or out of BFCache — it does neither. Useno-store. - Sending
Clear-Site-Dataover HTTP — ignored; it is only honored on a secure transport.
Platform-Specific Implementation
Set these headers per route. A site-wide no-store is a self-inflicted performance outage; route the directives so static assets stay cacheable and only authenticated responses are protected.
Nginx
# Sensitive authenticated routes only
location ~ ^/(account|banking|settings) {
add_header Cache-Control "no-store" always;
}
# Logout endpoint purges client state
location = /logout {
add_header Clear-Site-Data "\"cache\", \"cookies\", \"storage\"" always;
add_header Cache-Control "no-store" always;
}
The always flag emits the header on 4xx/5xx responses too, so an error page rendered on a sensitive route is not accidentally cacheable. Note that an add_header in a location replaces all inherited headers — re-declare any other security headers you need in these blocks. Verify: curl -sI https://example.com/account | grep -i cache-control
Apache
<LocationMatch "^/(account|banking|settings)">
Header always set Cache-Control "no-store"
</LocationMatch>
<Location "/logout">
Header always set Clear-Site-Data "\"cache\", \"cookies\", \"storage\""
Header always set Cache-Control "no-store"
</Location>
always ensures the directive is appended even on internally generated error documents, which the default onsuccess table skips. Requires mod_headers. Verify: apachectl -M | grep headers then curl -sI https://example.com/logout | grep -i clear-site-data
IIS
<location path="account">
<system.webServer>
<httpProtocol>
<customHeaders>
<add name="Cache-Control" value="no-store" />
</customHeaders>
</httpProtocol>
</system.webServer>
</location>
IIS scopes customHeaders with the <location path> element so only the matched route receives no-store; a header placed at the site root would apply everywhere. Remove any conflicting Cache-Control set by static-content modules on the same path. Verify in IIS Manager → HTTP Response Headers, then confirm with curl -sI.
Node/Express (Helmet)
// Helmet does not own Cache-Control; set it per route
app.get('/account', (req, res) => {
res.set('Cache-Control', 'no-store');
res.render('account');
});
app.post('/logout', (req, res) => {
req.session.destroy();
res.set('Clear-Site-Data', '"cache", "cookies", "storage"');
res.set('Cache-Control', 'no-store');
res.redirect('/');
});
Set the headers inside the route handler (not a blanket app.use) so only sensitive responses carry no-store; a global middleware would strip caching from your entire asset pipeline. Behind a TLS-terminating proxy, Clear-Site-Data is only honored if the response reaches the browser over HTTPS. Verify: curl -sI https://localhost:3000/account | grep -i cache-control
Cloudflare
Use a Response Header Transform Rule scoped by URI path. In Rules → Transform Rules → Modify Response Header, match URI Path starts with /account and set Cache-Control to no-store; add a second rule matching /logout that sets Clear-Site-Data. Critically, set Cache Rules → Bypass cache on the same path expression — otherwise Cloudflare’s edge may cache the response before your origin’s no-store is honored, since the edge applies its own cache logic. Verify: curl -sI https://example.com/account | grep -i cache-control
Verification & Diagnostic Workflows
A no-store that your origin emits but the CDN strips is worse than none — it gives false assurance. Verify at the edge the client actually talks to.
Header presence and exact value:
curl -sI https://example.com/account | grep -iE 'cache-control|clear-site-data'
# cache-control: no-store
Logout teardown:
curl -sI https://example.com/logout | grep -i clear-site-data
# clear-site-data: "cache", "cookies", "storage"
Confirm a shared cache is not retaining the page — request through the CDN edge and inspect the cache-status header:
curl -sI https://example.com/account | grep -iE 'cf-cache-status|age|x-cache'
# cf-cache-status: BYPASS
# (no Age header on a never-cached response)
A non-zero Age or a HIT/cf-cache-status: HIT on an authenticated route means a shared cache stored it — a leak.
Browser DevTools: open DevTools → Application → Storage. After hitting a no-store route, the Network panel’s request row should show (memory cache)/(disk cache) only for assets, never for the document. After logout, Application → Cookies and Application → Local Storage / Session Storage / IndexedDB for the origin should be empty. The Application → Back/forward cache panel (Chrome) reports whether the page is BFCache-eligible; a no-store document is reported as not eligible, which is the goal for authenticated pages.
BFCache check from the keyboard: load the authenticated page, log out, press the browser Back button. The page must not restore an authenticated view from memory. If it does, the document response lacked no-store.
CI/CD regression gate:
hdr=$(curl -sI https://example.com/account | grep -i '^cache-control:')
echo "$hdr" | grep -qi 'no-store' \
|| { echo "Sensitive route lost no-store"; exit 1; }
Troubleshooting, Misconfigurations & Safe Rollback
- Site-wide
no-storetanked performance → the directive was applied at the server root instead of on sensitive routes. Caching, CDN offload, and BFCache for benign pages all vanish. Scopeno-storeto^/(account|banking|...)and let static assets keeppublic, max-age. Rollback: remove the globaladd_header/Header setand re-add it perlocation/LocationMatch. Clear-Site-Data: "*"wiped more than intended →"*"includesexecutionContextsand clears Service Workers and Cache Storage, which can break an offline-capable app and force-reload other open tabs of the origin. Replace"*"with the explicit set you need, typically"cookies", "storage". There is no undo for purged data; rollback means changing the header so the next logout purges less.- CDN still serving a cached authenticated page → the edge cached the response before honoring
no-store, or a cache rule overrides origin headers. Add an explicit cache bypass on the path (Cloudflare Cache Rules,proxy_no_cache/proxy_cache_bypassin Nginx,CacheDisablein Apache). Confirm withcf-cache-status: BYPASS. - Back button restores the authenticated page after logout → the document was served with
no-cacheorprivateinstead ofno-store; onlyno-storekeeps a page out of BFCache. Switch the directive on the sensitive route. Clear-Site-Dataignored → values not quoted, served over HTTP, or the browser (Safari) does not support the requested type. Pair the header with server-side session destruction and cookie expiry so logout is correct regardless of client support.- Header present but proxy adds a duplicate
Cache-Control→ a reverse proxy injected its own directive. Browsers may honor the more permissive one. Normalize at a single layer; in Nginx useproxy_hide_header Cache-Controlbefore re-adding.
Frequently Asked Questions
What is the difference between no-store and no-cache?
no-store forbids any cache from storing the response and keeps it out of the back/forward cache — nothing persists. no-cache permits storage but requires revalidation with the origin before reuse; it does not stop on-disk persistence or BFCache. For sensitive pages you almost always want no-store.
Does Cache-Control: no-cache keep a page out of the back/forward cache?
No. Only no-store reliably makes a document BFCache-ineligible. If pressing Back after logout restores an authenticated view, the page was served with something other than no-store.
Does Clear-Site-Data work over plain HTTP?
No. The browser only honors it on a secure transport. Serve it over HTTPS, which is why pairing it with HSTS matters. It is also not universally supported, so it supplements — never replaces — server-side session invalidation.
Is Pragma: no-cache still needed?
No. Pragma is an HTTP/1.0 request-header mechanism; modern browsers do not treat a Pragma response header as authoritative for caching. Send proper Cache-Control directives. Keep Pragma only as a defensive note for ancient intermediaries; it is not a substitute for no-store.
Should I set no-store for the whole site?
No. A site-wide no-store eliminates browser caching, CDN offload, and BFCache for every page, degrading performance for no security gain on public content. Scope it to routes that render per-user or secret data.