Keep private pages out of browser cache without mistaking logout for erasure
Apply modern cache directives at the correct boundary, revoke server-side sessions, test browser history and intermediaries, and understand where bfcache changes the threat model.
A user signs out, presses Back, and sees the account page again. That can be a stored HTTP response, a browser back/forward snapshot, or a page that rendered cached API data. It does not automatically prove the server session still works—but it is a privacy failure on a shared device, and the possibility of a live session must be tested separately.
Separate four controls that are often collapsed into “no cache”
| Control | Question it answers |
|---|---|
| Session revocation | Will the server reject the old credential? |
| HTTP cache policy | May a browser or intermediary store/reuse the response? |
| History snapshot policy | Can browser navigation restore an in-memory document? |
| Client data lifecycle | Does JavaScript, a service worker, or local storage retain private data? |
No single header answers all four.
Use current response directives, not a folklore bundle
Cache-Control: no-store, private
Content-Type: text/html; charset=utf-8
RFC 9111 defines no-store as a direction not to store any part of the response. private prevents a shared cache from storing the response, while allowing a private cache unless another directive forbids it. Combining them makes the audience explicit and is useful defense in depth.
no-cache does not mean “do not store.” It allows storage but requires successful validation before reuse. Use it when a representation may be retained and efficiently revalidated; do not substitute it for no-store on highly sensitive pages.
Cache-Control: private, no-cache
ETag: "account-view-42-v17"
Correct the accepted answer’s historical compatibility advice
The accepted answer recommends Pragma: no-cache and Expires: 0 for old HTTP/1.0 clients. RFC 9111 now describes Pragma as a historical request compatibility mechanism and does not define it as a response replacement for Cache-Control. Modern applications should make Cache-Control authoritative. Add legacy fields only when measured compatibility requirements justify them; they do not strengthen session security.
Set policy on every sensitive response
Apply the header at the application or narrowly scoped reverse-proxy location so HTML, JSON, error responses, and redirects carrying private data share the policy:
// PHP, before any output
header('Cache-Control: no-store, private');
header('Content-Type: text/html; charset=utf-8');
Do not add a site-wide no-store rule blindly. It destroys caching for public assets and increases latency, bandwidth, and origin load. Conversely, a header only on the HTML shell is insufficient if /api/account returns the actual private data.
Make logout a server-side state change
POST /logout HTTP/1.1
Cookie: session=opaque-id
Origin: https://app.example.test
On receipt, revoke or rotate the server-side session, clear the cookie with matching attributes, protect the request against CSRF as appropriate, and redirect with a non-sensitive response. A logout that only deletes a browser cookie leaves copied or replayed credentials live.
Set-Cookie: session=; Max-Age=0; Path=/; Secure; HttpOnly; SameSite=Lax
Cache-Control: no-store
The Path and, if present originally, Domain must match the cookie being removed.
Diagnose the path with evidence
- Capture the sensitive response in browser developer tools with “Disable cache” off.
- Inspect the final response after CDN and proxy processing, not only application logs.
- Sign out, then replay the old authenticated request outside the browser. It must fail.
- Navigate Back and determine whether a network request occurs.
- Inspect service workers, Cache Storage, IndexedDB, local storage, and in-memory state.
- Repeat on a shared-cache path and with a second account to test cross-user isolation.
curl -i https://app.example.test/account \
-H 'Cookie: session=OLD_SESSION'
curl -i https://app.example.test/api/account \
-H 'Cookie: session=OLD_SESSION'
After logout, both should return an unauthenticated response and no private body. Redact credentials before preserving traces.
Understand the back/forward cache boundary
Modern browsers can restore an in-memory page snapshot without performing an HTTP cache lookup. Browser behavior evolves, and cache-control interaction is not uniform across all navigation scenarios. Treat this as an additional layer: listen for pageshow, revalidate authentication when a page is restored, and replace sensitive UI before showing data.
window.addEventListener("pageshow", async (event) => {
if (!event.persisted) return;
const response = await fetch("/api/session", {
credentials: "same-origin",
cache: "no-store"
});
if (!response.ok) location.replace("/login");
});
This is defense in depth, not the primary security boundary. An attacker can disable JavaScript; the API must reject the revoked session.
Clear client-owned private data deliberately
await caches.delete("private-api-v1");
sessionStorage.removeItem("account-view");
history.replaceState(null, "", "/signed-out");
Prefer never storing secrets in Web Storage. A service worker must not cache authenticated responses under a key that omits user identity, authorization state, or relevant Vary dimensions. Purging on logout reduces residue but cannot repair a cache that already leaked data between users; rotate credentials and investigate scope.
Verification matrix
- Fresh authenticated navigation returns the intended private data and
no-store. - Refresh and Back after logout reveal no private content.
- The old cookie fails at every private HTML and API route.
- A second user never receives the first user’s representation through CDN or proxy caching.
- Offline mode does not reveal a service-worker-cached private response.
- Error pages and redirects do not embed account data or cacheable tokens.
Failure modes and production trade-offs
no-store increases repeat transfers and prevents useful offline behavior. For moderately sensitive dashboards, private, no-cache plus validators may be acceptable; for health, finance, administrative, or identity data, the privacy benefit of no-store usually outweighs the cost. Measure origin capacity before rollout.
Do not use “clear site data on every logout” as an unreviewed hammer: it can remove unrelated origin data, disrupt multi-account workflows, and behave differently across clients. Do not append random query strings to private pages; cache busting changes cache keys but does not forbid storage.
Rollback and incident recovery
If the new policy overloads the origin, retain session revocation and private-cache isolation while rolling back incrementally to a reviewed revalidation policy. Never restore shared caching for user-specific responses as an emergency performance fix. If cross-user exposure occurred, purge affected CDN keys, revoke sessions, preserve cache configuration and logs, determine which identities and fields were exposed, and follow the incident-notification process.
Logout is proven by rejection at the server. Privacy is improved by cache policy. Browser history is tested as its own execution path.
Sources: the Stack Overflow question by Edward Wilde and accepted answer by BalusC (CC BY-SA), RFC 9111: HTTP Caching, and Chrome’s current cache-control and back/forward cache guidance.
Primary source: Review the official reference ↗