A cache can make a web application faster by reusing a response instead of asking the application to generate it again. That becomes a security problem when the reused response contains data for one particular user.
If a shared cache stores a personalized account page under a key that does not distinguish users, a later requester may receive the first user’s response. Authentication at the application can be perfectly correct and the data can still cross an authorization boundary because the second request never reaches that application logic.
The defensive mental model is simple: a shared cache is another component that can answer requests. Before allowing it to store a response, decide whether that response is genuinely reusable across the requests that can map to the same cache entry.
A cache hit can bypass application authorization
Consider an endpoint that returns the signed-in user’s profile:
GET /account/profileThe application normally authenticates the request, identifies the user, checks access, and returns that user’s details. Without caching, each request follows that path.
Now place a shared cache in front of the application:
request -> shared cache -> application
|
+-- cached response, if presentSuppose the cache key contains only the host and path. Alice requests /account/profile, the application returns Alice’s profile, and the cache stores it. Bob then requests the same path. From the cache’s point of view, the requests may look equivalent. If it serves the stored representation, Bob receives data that the application intended only for Alice.
The failure is not that the application forgot to authenticate Bob. The failure is that a component outside the application’s authorization decision reused an authenticated response under a broader identity than the response was allowed to have.
This gives a useful rule:
cache reuse scope must not be broader than response authorization scopeIf a response is authorized for one user, a cache entry that is reusable by every user is too broad.
Separate public caching from private state
Caching itself is not the problem. Public assets, documentation, product descriptions, and other representations that are intentionally identical for many users can often be shared safely when their cache policy matches that design.
Personalized responses require different treatment. HTTP cache controls can express that distinction. In particular, Cache-Control: private means a response may be stored by a private cache, such as one associated with a single user agent, but must not be stored by a shared cache. Cache-Control: no-store is stricter: caches must not store the response.
Those directives solve different problems. A response that is safe to retain in a user’s own browser cache but not safe to share can use a private caching policy. A response containing especially sensitive material, or one whose retention itself is undesirable, may justify no-store.
Do not treat private and no-store as interchangeable performance settings. Choose based on who may reuse the representation and whether it should be retained at all.
A simplified policy might look like:
Cache-Control: private, no-cacheHere, private restricts shared-cache storage. no-cache does not mean “do not store”; it means a stored response must be validated before reuse. If storage itself is unacceptable, use no-store instead.
That distinction is easy to miss and worth testing explicitly.
The cache key defines who shares an entry
Sometimes a response legitimately varies by request attributes. The cache must then distinguish the attributes that change the representation.
For example, a public page may return compressed or uncompressed content depending on request headers. HTTP’s Vary response header can tell caches that selected request-header fields affect representation selection.
Security-sensitive personalization is more difficult. It can be tempting to add a cookie or authorization value to the cache key and declare the response safe to share. That approach deserves caution.
First, the key must represent every authorization-relevant distinction that can change the response. Missing one dimension can merge users, tenants, roles, locales with sensitive differences, or other security contexts into the same entry.
Second, putting high-cardinality credentials directly into cache keys can create operational problems and may expose sensitive values through cache diagnostics, logs, or tooling if those systems record keys. Credentials should not be copied into observability data merely to make caching work.
Third, cache behavior is often distributed across application code, reverse proxies, content delivery networks, and managed platform defaults. A key that is correct in one layer does not guarantee another layer uses the same rules.
For ordinary authenticated pages, the simpler defensive choice is often not to place personalized responses in a shared cache at all. Use shared caching where reuse is intentionally public or where the architecture has a carefully designed, reviewed partitioning model.
Do not infer privacy from the request alone
A common configuration says something like “requests with an Authorization header are not cached.” That can be a useful safeguard, but it is not a complete model of response sensitivity.
Applications authenticate users in many ways. Session cookies are common. A request without an Authorization header can still produce private data. Conversely, an authenticated request might intentionally fetch a representation that is public and identical for everyone.
The response-producing application knows more about the meaning of the data than a generic cache does. Express cache policy deliberately in the response rather than relying only on heuristics at the caching layer.
Framework defaults can help, but security should not depend on remembering which controller methods happen to trigger those defaults. Sensitive endpoints should have testable response policies.
Be careful with errors and redirects too
Developers often think about caching successful 200 responses and forget that other responses can contain user-specific information.
An error page might include an account identifier, validation details, or a fragment of submitted data. A redirect can point to a user-specific destination. Some caching systems can cache response classes beyond ordinary success responses when configuration or explicit freshness information permits it.
The safe question is therefore not “Is this a normal page?” It is “Could this response reveal or encode state that belongs to a narrower audience than the cache entry?”
Apply the privacy decision to the response semantics, including exceptional paths. An endpoint that marks its normal response private but lets an error handler overwrite the headers can still leak information through caching.
Purging is recovery, not the primary control
If a private response has entered a shared cache, removing it quickly matters. Purge or invalidation mechanisms can reduce the period during which the wrong response is available.
They are not a substitute for correct cache policy. In a distributed cache, copies may exist at several points of presence or in multiple tiers. Purge behavior can fail, be delayed, or target a narrower key than the one that was actually populated.
Incident response should assume that a wrongly cached private representation may already have been served. In addition to invalidating it, determine which cache key was used, how long the entry was reusable, which request paths could receive it, and whether logs can establish who received cached responses without exposing more sensitive data.
Then fix the policy that allowed storage or cross-user reuse. Otherwise the next request can simply repopulate the bad entry.
Test from two security contexts
A single-user test can confirm that caching works while completely missing the security failure. The useful test involves at least two distinct security contexts.
For a response that should never cross users:
- Authenticate as user A and request the endpoint.
- Confirm the response contains only data authorized for A.
- Authenticate independently as user B and request the same URL through the same caching path.
- Confirm B receives B’s authorized representation, not A’s cached response.
- Inspect the cache-related response headers and cache diagnostics to verify whether the response was stored or served from cache as intended.
Also reverse the request order and test anonymous access if the endpoint supports it. Cache bugs can be directional: an anonymous representation might overwrite an authenticated one, or an authenticated response might become the first entry later reused for anonymous traffic.
When the infrastructure supports it, automated tests should exercise the deployed proxy or cache configuration rather than only the application server. Application unit tests cannot prove how an external shared cache interprets the response.
Know the threat model and residual risk
Private-response cache controls reduce the risk that shared caching accidentally turns a response for one security context into a response for another. They are especially relevant when reverse proxies, gateways, or content delivery networks can answer requests before application authorization runs.
They do not fix broken authorization inside the application. If the origin itself returns Alice’s data to Bob, Cache-Control: private merely tells shared caches not to amplify that mistake. The controls also do not protect data after a legitimate recipient’s device is compromised, and they do not replace transport encryption.
Private browser caching has its own threat model. On a managed single-user device it may be acceptable for many account pages. On shared devices, highly sensitive responses may need stricter retention choices. Logout behavior, browser history, downloaded files, and local storage are separate concerns and should not be assumed to disappear because a shared cache was configured correctly.
Make cacheability an explicit security decision
The easiest cache policy to reason about is one that follows the data’s intended audience. Public responses can be designed for broad reuse. User-specific responses should remain private unless the system deliberately partitions cache entries by every security context that affects the representation and the operational benefit justifies that complexity.
For each authenticated endpoint, ask who is allowed to receive an identical response. Then make the cache policy no broader than that audience and test it through the real caching path with different users.
That turns caching from an invisible performance layer into what it actually is: another request handler whose authority to reuse data must respect the same security boundary as the application behind it.