A reverse proxy can receive two requests that look different to an application and identical to its cache. That disagreement is enough to turn an ordinary performance feature into a cross-user security boundary.

Shared HTTP caches are built around equivalence. A cache key decides which requests may reuse the same stored response. The origin application makes a separate decision about which request properties influence its output. Security problems appear when those two models diverge: the origin varies a response on data that the cache does not include in its identity for that response.

The resulting failures are often described as cache poisoning, but the label covers several mechanisms. The important distinction is whether an attacker can cause a shared cache to store a response shaped by attacker-controlled input and later serve that representation to other clients. That requires more than an unusual header or query parameter. It requires a mismatch between response generation, cache-key construction, and cacheability.

A cache key is an equivalence policy

A typical cache key contains the request method, host, path, and some representation of the query string. Real deployments are more varied. CDNs and reverse proxies may normalize query parameters, exclude selected parameters, vary on chosen headers, partition by device class, or apply route-specific policies. Applications behind those layers may consume a much larger request surface.

Consider an application that uses a forwarded host header to construct an absolute URL in a page. If the edge cache ignores that header, a request carrying an attacker-selected value can produce a distinct response while mapping to the same cache key as an ordinary request. If that response is cacheable, later clients can receive content generated from the attacker’s input.

The same pattern can involve headers used for routing, localization, scheme detection, content negotiation, or application-specific behavior. Query handling can create a similar gap when the cache strips a parameter that the origin still interprets. The security property is therefore not tied to one HTTP field. It depends on agreement about every input that can materially change a shared response.

This also separates cache poisoning from a simple reflected-input defect. Reflection alone affects the request that supplied the input. A shared cache can extend the effect across requests, users, and time. The cache becomes an amplifier because it preserves one representation and treats later requests as equivalent to the request that created it.

Normalization can create disagreement without obvious misconfiguration

Caching layers rarely forward requests byte for byte. They canonicalize paths, parse query strings, combine or discard headers, and may apply product-specific rules before selecting an object. The origin stack performs its own parsing after that transformation. Multiple proxies can add still more interpretation.

A dangerous case exists when two components disagree about semantic identity. One component may consider two query strings equivalent after dropping an analytics parameter, while the application assigns that parameter functional meaning. A proxy may trust one host-related field for its key while the framework uses another when generating links. Path decoding and normalization can also produce divergent views when components process encoded delimiters or repeated separators differently.

Not every parser difference is exploitable. The divergent input must influence a response that can enter a shared cache, and subsequent requests must select that stored object. Cache directives, authentication state, response status, request method, cookies, and edge policy all affect that outcome.

This conditional nature matters during assessment. Sending an unusual header and observing it in a response does not establish cache poisoning. A credible finding needs evidence that the response was stored under a key that does not distinguish the influencing input and that another request can retrieve the stored variant without supplying that input.

Cacheability is part of the authorization model

Teams often treat cache control as a performance concern, but shared caching determines which response may cross a request boundary. A private account page cached without appropriate separation can disclose data even when its cache key contains every syntactic input correctly. The problem in that case is not an unkeyed input; it is that a user-specific representation was eligible for shared reuse at all.

HTTP provides response directives for expressing caching intent, but deployment behavior still deserves verification. Cache-Control: private indicates that a response is intended for a private cache rather than a shared cache. no-store directs caches not to store the response. Vary identifies request header fields that participated in selecting the representation, allowing caches to distinguish stored variants according to those fields.

Those controls solve different problems. Vary is not a general instruction to include arbitrary request data in every implementation’s primary key, and it cannot compensate for an application that places sensitive personalized content into a broadly cacheable response. Likewise, adding every header to a cache key is not a practical security strategy. It can destroy cache efficiency, create excessive object cardinality, and still miss inputs transformed elsewhere in the request path.

The stronger design starts by deciding which responses are safe to share. Only then does key design determine which requests are equivalent within that shareable set.

Multiple cache layers make the effective policy harder to see

Modern request paths commonly contain more than one cache. A CDN may sit in front of a cloud load balancer, an ingress proxy, and an application framework with its own fragment or response cache. Each layer can use different keys and different rules for honoring cache-control metadata.

This composition creates operational blind spots. An origin developer may inspect framework configuration and assume a header participates in variation, while the outer CDN has already collapsed those requests into one object. Conversely, an edge policy may appear safe until an internal cache stores a response under a broader identity.

Observability needs to expose the layer that actually served the response. Age metadata, cache-status headers, provider diagnostics, request tracing, and controlled tests can help distinguish an origin response from a cache hit. These signals are deployment-specific, and some are intentionally hidden from public traffic, so security review often needs configuration evidence alongside black-box behavior.

Purge behavior deserves similar attention. Removing a vulnerable response from one layer does not guarantee that every downstream or regional cache has discarded it. Incident handling needs a model of the complete cache topology, including invalidation scope and propagation characteristics.

Defensive design depends on reducing semantic gaps

The most reliable control is to keep cache policy close to application semantics. Inputs that affect a shareable representation need consistent treatment across the edge and origin. Inputs that should never influence public output should be rejected, normalized, or replaced with values derived from trusted configuration rather than merely added to an ever-growing key.

Host and scheme generation are good examples. An application that already has a canonical public origin usually gains little from constructing security-sensitive URLs from arbitrary inbound forwarding metadata. Restricting accepted hostnames and defining trusted proxy boundaries reduces both cache risk and other classes of request-confusion defects.

For routes that mix public and personalized content, architectural separation is often safer than intricate key variation. A public shell can remain broadly cacheable while authenticated or user-specific data is fetched through a path that is explicitly private. This makes the sharing boundary visible instead of encoding it in a long list of headers and cookies.

Configuration review should also treat cache-key changes as security-relevant changes. Removing a query parameter from a key may deliver a major hit-rate improvement, yet that optimization is safe only if the origin does not use the parameter to alter the shared representation. The same scrutiny applies when adding middleware that starts consuming a previously ignored header.

The security property is agreement

A shared cache is safe when its notion of request equivalence matches the application’s notion of response equivalence for content that is permitted to be shared. That property sounds simple, but it spans infrastructure, HTTP parsing, framework behavior, and application logic.

The practical risk grows when ownership is split. Edge teams tune cache efficiency, application teams add request-dependent behavior, and security teams inspect each layer independently. None of those changes needs to look dangerous in isolation. The defect emerges from the composition.

Treating the cache key as an explicit security boundary changes the review question. The issue is not merely whether a page is cached. It is whether every request mapped to the same shared object is genuinely entitled to receive the same representation. That is the invariant a cache architecture has to preserve.