Cache Keys Define the Security Boundary of Shared HTTP Responses
A reverse proxy receives two requests for the same URL. One carries a header that changes the origin response; the other does not. If the proxy stores the first response under a key that ignores that header, the second request can receive content generated from state it never supplied. The cache is operating correctly according to its key, yet the key has merged two requests that the application treats as distinct.
That mismatch is the core security condition behind many cache poisoning and cross-user response mix-up failures. Shared caching is not merely a performance layer. It creates a reuse boundary: requests considered equivalent by the cache can share one stored representation. Security depends on that equivalence relation matching the origin’s response semantics closely enough for every response admitted to shared storage.
A cache key is an equivalence rule
HTTP caching permits stored responses to satisfy later requests when protocol conditions allow reuse. In practice, reverse proxies, content delivery networks, gateways, and application caches construct keys from some combination of scheme, authority, path, query data, method, and selected request fields.
The exact composition is implementation and configuration specific. RFC 9111 defines HTTP caching semantics, but it does not require every deployment to use one universal cache-key formula. A platform can also add normalization, ignore selected query parameters, or vary its key according to product-specific rules.
That flexibility is useful for hit rate and operational control, but it means a cache key is part of application security architecture. If two requests map to the same key, the cache is asserting that a stored response for one is eligible to represent the other, subject to the cache’s remaining reuse checks.
An origin may make a finer distinction. It might select content from a request header, tenant identifier, locale cookie, feature flag, rewritten path, device hint, or authentication state. Any response-changing input omitted from the effective cache partition can create a mismatch.
Not every omission is exploitable. The response must also be storable, reusable for another request, and influenced in a security-relevant manner. Those conditions matter when assessing an actual system.
Vary extends selection but does not repair every key mismatch
The Vary response field tells caches that selected request header fields participated in choosing the representation. A cache evaluating a stored response with Vary must account for the named fields when deciding whether that response matches a later request.
This is a protocol mechanism for representation selection, not a general declaration of every application dependency. It only covers request fields that the origin names, and its effect depends on the cache honoring HTTP semantics for that response.
An origin that changes output based on Accept-Language can use Vary: Accept-Language so variants are not treated as interchangeable. The same pattern can apply to other request header fields where shared caching remains appropriate.
Several important inputs sit outside that model. URL components are normally part of primary cache selection rather than Vary. Application state derived from server-side session data is not a request header field that Vary can name. A proxy may also transform or remove request data before the origin sees it, leaving cache selection and origin processing with different views of the request.
Vary: * has special semantics: a stored response with that value cannot be selected for reuse based on matching request fields. It is not a practical substitute for enumerating arbitrary hidden dependencies while retaining ordinary shared-cache reuse.
Unkeyed inputs turn origin behavior into shared state
A dangerous pattern appears when an intermediary forwards a request property to the origin but excludes it from cache selection.
Suppose an application uses a forwarded host-like field to build an absolute URL in a response. If an edge cache keys only on the public authority and path, an attacker-controlled forwarded value may alter the generated response without creating a distinct cache entry. If that response is cacheable, later requests can receive the altered representation.
The specific field is not the essential issue. Similar structure appears whenever three conditions align:
- a request-controlled value reaches response generation;
- the cache does not separate requests on that value; and
- the resulting response can enter a shared cache and be served to other requests.
This is often called web cache poisoning when an attacker can deliberately seed a shared entry with harmful content. The same architectural mismatch can also cause accidental cross-context response reuse without an adversary actively poisoning the cache.
Header trust policy therefore matters before caching policy. Fields inserted by a trusted proxy are different from fields accepted directly from an external client. A deployment that relies on forwarded metadata should define which hop creates it, strip conflicting client-supplied values, and keep the cache’s request view aligned with the origin’s trusted view.
Normalization can collapse distinct application requests
Caches frequently normalize request targets to improve reuse. A deployment may sort or discard selected query parameters, normalize path forms, lowercase selected components, or map several public routes onto one internal cache namespace.
Normalization is safe only when the origin treats the collapsed forms as equivalent for the cached representation.
Ignoring tracking parameters is a common performance optimization. If a parameter truly has no effect on response generation, removing it from cache selection does not change representation boundaries. If application code later starts using that same parameter for content selection, redirects, access decisions, or template output, the optimization becomes stale security state.
The reverse mismatch also occurs. A cache may distinguish values that the origin normalizes to one meaning. That usually reduces hit rate rather than causing cross-request reuse, but it can complicate invalidation and security analysis because the cache and origin no longer share a common model of resource identity.
Canonicalization deserves particular care at multi-layer edges. A CDN, load balancer, framework router, and application may each parse paths and queries. Security analysis has to follow the representation from the request form used for cache lookup through every transformation that affects origin routing or output.
Authentication changes the admissible reuse set
Responses associated with authenticated requests need explicit treatment. HTTP caching has rules governing when shared caches may store and reuse responses to requests carrying Authorization, and response directives can permit or prohibit particular forms of shared reuse.
Application cookies introduce a separate concern. A response can vary on cookie-derived identity or session state even when no Authorization field is present. A shared cache that admits such a response without a suitable partition can expose personalized content across users.
Cache-Control: private directs shared caches not to store a response, while no-store prohibits storage by caches more broadly. These directives are often safer than attempting to enumerate identity-bearing request properties when a representation is inherently user specific.
A response that is public in principle can still contain small personalized fragments. That architecture needs a deliberate split between shared and private state. Treating an entire personalized page as globally reusable because most bytes are common creates a much larger trust boundary than the application semantics support.
Cache configuration also has to account for redirects and error responses when the platform can store them. A poisoned redirect can influence navigation even though it contains little body content, and a cached authorization-dependent error can deny access to requests that should receive a different result.
Multiple cache layers can disagree
Production request paths often contain more than one cache: a CDN at the edge, a reverse proxy near the service, framework-level response caching, and perhaps application-specific object caches.
Each layer can define a different key and a different normalization policy. A request may miss at the outer cache, hit at the inner cache, and return a representation whose provenance is invisible to the outer layer. Debugging only the first cache therefore does not establish the effective reuse boundary.
Layered caching also affects invalidation. Purging one namespace does not necessarily remove a conflicting entry elsewhere. Security remediation after a cache-key defect should identify every layer that may retain affected representations and the maximum retention allowed by its freshness and eviction rules.
Observability should expose enough information to reconstruct cache decisions without leaking sensitive key material. Useful signals include cache status, selected route, normalized target, cache policy identifier, and which layer produced a hit. Raw authentication tokens, full cookies, and secret-bearing query values do not belong in diagnostic logs merely to make cache analysis easier.
Safe reuse requires a stable representation contract
The strongest design is not an ever-growing cache key. Adding every request field destroys reuse and can create attacker-controlled key cardinality. The goal is a narrow, explicit representation contract: identify which inputs are allowed to affect a shared response, then make cache selection match those inputs.
That contract should include transformations performed before the origin, trusted metadata inserted by intermediaries, response directives emitted by the application, and platform-specific cache configuration. Changes on either side can invalidate earlier assumptions.
Where a response depends on identity, authorization, tenant state, or other context that is difficult to partition safely, excluding it from shared caching is often the cleaner boundary. Where shared caching is required, key dimensions should be finite, canonicalized consistently, and derived from values whose trust semantics are explicit.
Cache poisoning defenses are therefore less about filtering a catalog of suspicious headers than about preserving one invariant: no shared cache entry should represent requests that the origin considers security-relevantly different. Once that invariant fails, a performance optimization becomes a mechanism for carrying one request’s influence into another request’s response.