How Web Caching Works

A cache sits between clients and the origin server. When a request arrives, the cache computes a cache key β€” typically URL + Host + (selected headers) β€” and checks whether it has a stored response for that key. A cache hit serves the stored response without contacting the origin. A cache miss forwards to the origin, caches the response, and returns it.

The cache key determines equivalence. Two requests with identical cache keys are treated as requests for the same resource. The cache serves the same response to both. Request components not included in the cache key are ignored for the purpose of cache lookup β€” they are unkeyed.

Both cache poisoning and cache deception exploit mismatches: cases where the cache and origin disagree about what makes two requests equivalent, or where caching rules apply inappropriately to private or dynamic content.

Cache Poisoning

Cache poisoning causes the cache to store a malicious response for a URL and serve it to every subsequent visitor β€” without those visitors sending any special request. The attacker sends one crafted request; the cache amplifies it to all users.

AttackerSends request to /page with crafted unkeyed header containing XSS payload
Cache MissCache forwards to origin; origin reflects header value in response
Cache StoreCache stores the poisoned response keyed to /page
VictimsSubsequent visitors receive the poisoned response with XSS payload

The attack requires two conditions: the origin must reflect some part of the attacker's request in the response (the injection point), and that reflected component must be unkeyed by the cache (so the poisoned response gets stored for the normal URL). A reflected XSS that requires a specific query parameter would normally only affect one victim who clicks a crafted link. If that parameter is unkeyed, the poisoned response is cached for the clean URL and served to all visitors.

Exploiting Unkeyed Headers

CDNs commonly add proprietary headers to identify request metadata β€” scheme, original host, client IP, protocol version β€” that the origin uses to construct responses. These headers are often not included in the cache key because they're "infrastructure information" rather than user data. But if the origin reflects them in the response, they become injection vectors.

Common unkeyed headers that have been used in cache poisoning:

  • X-Forwarded-Host β€” many frameworks use this to construct absolute URLs in redirects and Open Graph tags. Reflected without sanitisation, an attacker can inject a malicious domain.
  • X-Forwarded-Proto β€” used by origins to construct scheme-aware redirect URLs. If reflected, can be used to downgrade to HTTP.
  • X-Original-URL / X-Rewrite-URL β€” some reverse proxies pass these to override the request path. A cacheable response on /static/main.js that reflects the rewritten URL could be poisoned.
  • Accept-Language / Accept-Encoding β€” if the origin serves different content based on these but the cache doesn't vary on them, an attacker requesting with unusual values can poison the cache entry for default-language users.
# Cache poisoning via X-Forwarded-Host # Attacker sends: GET /home HTTP/1.1 Host: www.example.com X-Forwarded-Host: attacker.example.com # Origin reflects in response body (unescaped): <link rel="canonical" href="https://attacker.example.com/home"> <script src="https://attacker.example.com/static/app.js"></script> # Cache stores this as the response for GET /home on example.com # All subsequent visitors load scripts from attacker.example.com

Cache Deception

Cache deception is the inverse attack. Instead of making the cache serve attacker-controlled content, it makes the cache serve victim-controlled private content to the attacker. The goal is to trick the cache into storing a response that contains sensitive user data, then retrieve it from the cache without authentication.

The typical attack pattern exploits path confusion between the cache's routing rules and the origin's URL handling. Caches often assume that URLs ending in .css, .js, or .jpg are static assets and cache them aggressively regardless of authentication status. If the origin ignores the file extension and serves the same personalised response for /api/profile/me.css as for /api/profile/me, the cache will store that personalised response under the static-looking URL.

Attack flow: attacker sends victim a link to https://example.com/account/profile.css. Victim's browser follows the link while authenticated. The origin ignores the .css extension and returns the victim's profile page. The cache, seeing a .css extension, stores the response. The attacker requests the same URL without authentication and receives the cached personalised profile data.

Impact: Cache deception has been used to extract account information, session tokens, private messages, and payment details from major platforms. Once stored in the cache, the data is accessible from any network location without authentication credentials.

Testing for Both Attack Types

For cache poisoning, the methodology is: identify unkeyed inputs, check if they are reflected in the response, and confirm the response is cached (use cache-busting parameters to force cache misses during testing). Tools: Param Miner (Burp extension) automates discovery of unkeyed parameters and headers.

Cache indicators to look for in responses: X-Cache: HIT, Age: header with non-zero value, CF-Cache-Status: HIT (Cloudflare), X-Varnish:. A response with Age > 0 was served from cache.

For cache deception, append unexpected extensions to authenticated endpoints and check whether the response is cached and retrievable without the session cookie. Test paths like /account.css, /profile.jpg, /dashboard/index.html.

Prevention

  • Never reflect unvalidated request headers in responses: If X-Forwarded-Host must be used for URL construction, validate it against a whitelist of known domains. Do not reflect arbitrary header values.
  • Correct Vary header configuration: Include all request components that affect the response in the Vary response header. If Accept-Language changes your response, set Vary: Accept-Language.
  • Authenticated endpoints must not be cacheable: Set Cache-Control: no-store (not just no-cache) on all responses that contain user-specific data. Verify at the CDN layer that authenticated responses are not stored.
  • Strict URL normalisation at the origin: Reject requests with unexpected path extensions to authenticated routes. A request to /api/profile.css should be a 404, not the same response as /api/profile.
  • CDN cache key configuration audit: Review what your CDN includes and excludes from cache keys. Explicitly add headers your origin uses to construct responses to the cache key, or strip those headers at the CDN before forwarding.

The root cause in both attacks: A mismatch between what the cache considers equivalent and what the origin serves. Fix it by ensuring that everything affecting the response is either in the cache key or not cacheable at all.