The origin checks authorization correctly. The first user receives the right invoice. The second user receives the first user’s cached response without the origin running at all.
This is not necessarily a broken authentication library. It is a mismatch between the application’s representation boundaries and the cache’s reuse boundaries.
Every request allowed to reuse one cached representation must be entitled to receive that exact representation.
A cache hit is a data-access decision, even when the component making it has no concept of tenants or permissions.
Enumerate what changes the response #
Start with one route and list all inputs that affect its bytes: tenant, principal, role, locale, query parameters, entitlement, feature configuration, and selected resource version.
Then inspect the actual cache key at every layer. A correct application cache does not compensate for an unsafe reverse proxy. A correct CDN policy does not compensate for a browser or service worker that reuses account-specific content after a user switch.
browser -> service worker -> CDN -> reverse proxy -> application cache -> originAsk which layers can answer without re-running authorization and what evidence makes that safe.
Understand the directives you deploy #
RFC 9111 distinguishes shared and private caches. A private response is not for shared-cache storage. No-store prohibits storing the response under the protocol’s rules. No-cache permits storage but requires validation before reuse.
For a sensitive account endpoint, a conservative response policy is:
Cache-Control: private, no-store
Content-Type: application/jsonThat is a deployment policy choice, not encryption or a substitute for access control. It does not erase copies already saved elsewhere, and custom application caches need their own equivalent enforcement.
The RFC also specifies restrictions for shared reuse of responses to requests containing Authorization, with explicit exceptions. Do not depend on a header being present if authentication actually uses cookies or a proxy has transformed the request. See RFC 9111.
Vary is not an authorization system #
Vary tells a conforming cache which request headers distinguish representations. It does not prove the header values are trustworthy, establish a tenant relationship, or guarantee a particular CDN supports the desired key policy.
Putting a raw bearer token into an application cache key can create unbounded cardinality and credential-handling risks. Prefer bypassing shared response caching for personalized data unless a reviewed design can establish a bounded, verified identity partition.
Never vary on an attacker-controlled tenant header while the application authenticates using a different source. Identity normalization must happen before the key is selected.
Split public data from private overlays #
A product page may contain public descriptions and customer-specific discounts. Caching the whole response forces the public representation to inherit the strictest private constraints.
An alternative is to cache a public product representation and fetch the private price or account overlay separately. This adds a request and requires careful loading behavior, but it creates an auditable reuse boundary.
Another option is an application cache behind authorization, keyed by verified tenant and policy version. That can reduce expensive computation while keeping authorization in every request path. It is not equivalent to serving a shared CDN response before checking identity.
Freshness is also a permission question #
Suppose a user loses access after a response enters a private or identity-partitioned cache. If future hits bypass authorization, the TTL becomes an access-revocation delay.
Choose the permitted window deliberately. Sensitive administrative data may require reauthorization on every request. Lower-risk personalized content may accept a short window. If permissions change, invalidate every derived representation that relied on them—or version the permission context and prove old keys cannot be reached.
Do not add stale serving to an authenticated route solely to improve availability. Stale content can mean stale authorization.
Test cache hits with different identities #
A security test must warm the cache; testing only cold responses misses the failure.
- User A requests a tenant-specific resource and populates the cache.
- User B requests the same URL with different credentials.
- An anonymous client requests it.
- A’s permissions are revoked, then A requests it again.
- Repeat through every public hostname and relevant proxy path.
Assert bodies, headers, status codes, and cache-hit evidence—not just whether a login redirect occurred. Include error responses: a cached denial can lock out an authorized user, while a cached success can disclose data.
Also test query normalization, trailing slashes, alternate encodings, and resource identifiers that look like static filenames. Security depends on every layer agreeing on route identity.
The CTO decision #
Treat cache policy as part of data classification and authorization review. Require an owner for each shared representation, documented key inputs, a revocation window, and cross-identity hit tests before rollout.
The performance win is valuable only when reuse is safe. A high cache-hit ratio cannot compensate for returning the right data to the wrong person.