Athena Wiki

Web Cache Poisoning & Deception

webadvanced

Abuse caching layers to serve attacker-controlled responses (poisoning) or to capture victims' private data (deception) in CTF web challenges.

webcache-poisoningcache-deceptionunkeyed-headersparam-minercdnxss

Web Cache Poisoning & Deception

Caches sit between the client and the origin server (CDNs like Cloudflare, Akamai, Fastly, reverse proxies like Varnish/nginx, and application caches) to serve a stored response to many users. Two distinct attack classes abuse that shared storage:

  • Web cache poisoning — you trick the cache into storing a malicious response that is then served to other victims. The payload travels outward.
  • Web cache deception — you trick the cache into storing a victim's private response (their account page, API token) so that you can fetch it from the cache key. The data travels inward to you.

Both exploit the same root cause: a discrepancy between how the cache and the origin decide what a request "is" — what gets stored, and under which key.

Foundational reading

PortSwigger's Practical Web Cache Poisoning and the 2024 Black Hat paper Gotta cache 'em all are the canonical sources, alongside the Web cache poisoning and Web cache deception Web Security Academy paths.


Cache Keys: Keyed vs Unkeyed Inputs

When a cache receives a request it builds a cache key — a fingerprint of the parts of the request it considers significant. Typically this is the request line (method + path) and the Host header, sometimes a few more. If a later request produces the same key, the cache serves the stored response without contacting the origin.

Everything not in the key is an unkeyed input. If the origin reflects or acts on an unkeyed input but the cache ignores it for keying, you have the core primitive for poisoning: you send a request whose key matches what victims will request, but whose unkeyed input carries your payload. The poisoned response is cached and served to everyone.

Cache key  = GET /home + Host: example.com          <-- what the cache compares
Unkeyed    = X-Forwarded-Host, X-Forwarded-Scheme,  <-- ignored for keying but
             X-Host, cookies, some query params          may reach the origin

Confirm caching first

Look for response headers that prove a cache is in play and whether your request was a hit: X-Cache: hit/miss, CF-Cache-Status, Age, X-Cache-Hits, Cache-Control. A rising Age and a hit after your first request mean the response is being stored.


Finding Unkeyed Inputs with Param Miner

Param Miner is the open-source Burp extension that automates discovery of unkeyed headers, cookies, and query params. It guesses names from a large wordlist and watches whether each one changes the response while not changing the cache key.

Send the target request to Burp Repeater and add a cache buster — a unique, keyed value so your probes never collide with the real cached entry, e.g. GET /home?cb=8f3a1 or a unique Host. This stops you from poisoning the real key while testing.

Right-click the request and choose Extensions → Param Miner → Guess headers (and Guess cookies / Guess GET parameters). Param Miner sends its wordlist of header names and diffs the responses.

Review the Issues / extension output for "Identified the following secret input" findings, e.g. X-Forwarded-Host, X-Forwarded-Scheme, X-Original-URL, X-Host. These are candidate unkeyed inputs.

For each candidate, confirm in Repeater that it (a) changes the response and (b) does not change the cache key (the response can be retrieved by a clean request that omits the header).

The "cache buster" mindset

Never test poisoning against the path real victims hit. Always work behind a buster query/parameter, then only "fire for effect" against the live key once your payload is fully built — otherwise you DoS yourself or alert defenders.


Classic Reflected-Header Poisoning

The textbook case: the origin reflects an unkeyed header into the response body, often when building absolute URLs for assets.

The origin uses X-Forwarded-Host to construct an absolute script src:

<!-- origin response, host taken from X-Forwarded-Host -->
<script src="https://example.com/static/analytics.js"></script>

Other classic reflections: X-Forwarded-Scheme/X-Forwarded-Proto to force an insecure redirect loop, X-Original-URL/X-Rewrite-URL to change routing, and reflected unkeyed query parameters.

Encoding matters

The cache may key on the raw query string while the origin URL-decodes it, or vice versa. A value that is harmless to the cache key but decodes to a payload at the origin (e.g. ?param=valid&utm=%22><script>...) is a recurring poisoning trick. Mind which layer normalizes.


Web Cache Deception via Path Confusion

Deception flips the goal: instead of pushing your payload out, you cache someone else's sensitive page. You craft a URL that the origin treats as the dynamic, authenticated endpoint, but the cache treats as a static, cacheable file. Send the victim that URL (or have the admin bot visit it); their private response gets stored under a key you can then request unauthenticated.

The discrepancy lives in how each layer parses paths. The PortSwigger 2024 research Gotta cache 'em all catalogs these as cache rules the attacker subverts.

Origin vs cache normalization labs

The Web Security Academy distinguishes origin server normalization (origin decodes/resolves, cache does not) from cache server normalization (cache decodes, origin does not). Determine which side normalizes by probing how each layer responds to an encoded dot-segment or delimiter, then pick the payload that desynchronizes them.


Fat GET

Some frameworks read request parameters from both the query string and the body, even on a GET. A "fat GET" smuggles a body the cache ignores for keying but the origin parses:

GET /home?param=valid HTTP/1.1
Host: example.com
Content-Length: 23

param="><script>...</script>

The cache keys on the path/query only; the origin reflects the body parameter into the cached page. Same idea applies to parameter pollution where the cache keys on the first occurrence and the origin uses the last (?x=1&x=payload).


DoS via Cache Poisoning

You do not always need code execution — poisoning the cache with a broken response is a denial of service for every subsequent visitor.

GET / HTTP/1.1
Host: example.com
X-Forwarded-Host: a."><script>x</script>     # breaks page rendering for all

GET / HTTP/1.1
Host: example.com
X-Oversized-Header-Value: AAAA...AAAA         # origin 400s; the 400 gets cached

Cache-poisoned DoS (CPDoS) variants: oversized headers, illegal header characters, or a poisoned HTTP method override that makes the origin return an error which is then cached and served to all users.


Chaining

Cache poisoning is most powerful as a delivery mechanism for another bug, turning a self-only or low-impact issue into a cache-wide mass exploit.

  • + XSS — a reflected-header or unkeyed-param reflection that you could only fire against yourself becomes stored, persistent XSS against every cache consumer. See Reflected XSS and DOM XSS.
  • + Open redirect — poison a cached Location so every visitor is bounced to your phishing/credential page.
  • + Cookie/header reflection — reflect a cookie value into the body to cache a payload, or chain with CSP Bypass when an unkeyed header controls a script source the CSP otherwise allows.

Defenses

  • Prefer not caching authenticated or per-user responses at all; segment static and dynamic origins so a static cache can never see private content.
  • Cache only by an explicit allowlist of paths/extensions, not by guessing.

Checklist

  • Confirm a cache is present (X-Cache, Age, CF-Cache-Status) and observe hit/miss behavior.
  • Add a cache buster before testing; never probe the live key.
  • Run Param Miner to enumerate unkeyed headers, cookies, and parameters.
  • For each unkeyed input, verify it changes the response but not the cache key.
  • Test reflected-header poisoning (X-Forwarded-Host, X-Forwarded-Scheme, X-Original-URL).
  • Try fat GET and parameter-pollution reflections.
  • For deception: find a dynamic endpoint and confuse the path with .css/.js suffixes, /static/..%2f traversal, and ; / %23 / %3f / %0a delimiters.
  • Determine whether the cache or the origin normalizes encoded segments.
  • Consider DoS via oversized/illegal headers if a malformed response is cacheable.
  • Chain with XSS or open redirect for a cache-wide payload.

See Also


Last updated on

On this page