root@thehacksparrow:~/writeups$ SYSTEM ONLINE
root@sparrow:~/writeups$ cat vulnerability-pattern-cache.md
// bug-bounty

Cache Poisoning & Cache Deception

23 Jul 2026 · 3 min read

What it is

Two related but distinct bugs. Cache poisoning: an unkeyed input — a header or query param the cache doesn't include in its cache key — changes the response, and the cache then serves that poisoned response to every other visitor. Cache deception: tricking a cache into storing a private, personalized response under a URL that looks static, then fetching that same URL unauthenticated to read someone else's data.

Why it matters

The blast radius is the whole cache, not just your own request — one poisoned entry affects every visitor to that URL until it expires or gets purged. That's what turns a 'reflection' bug into something worth taking seriously.

Where to look

  • Anything sitting behind a CDN or reverse-proxy cache
  • Look at
    Vary
    ,
    Cache-Control
    and
    Age
    headers
  • Endpoints that reflect
    X-Forwarded-Host
    ,
    X-Forwarded-Scheme
    or
    Host
    into the response

How I test it

  1. Identify caching:
    Cache-Control
    ,
    Age
    ,
    X-Cache
    headers, or just suspiciously consistent response timing.
  2. Add an unkeyed header —
    X-Forwarded-Host
    ,
    X-Forwarded-Scheme
    ,
    X-Original-URL
    — with an attacker-controlled value and check if it reflects in the body, a redirect, or a script src.
  3. Confirm the poisoned response actually gets cached and served back to a second, unauthenticated request from a different session — without this step, all you have is reflection, not poisoning.
  4. For cache deception: append a fake static-looking suffix to a private authenticated URL (
    /account/settings/x.css
    ) and check whether the personalized response gets cached and can then be fetched without authentication.

Hunting checklist

  • Confirmed a caching layer exists
  • Tried at least one unkeyed header for reflection
  • Verified the poisoned response is served to a second, unauthenticated, different-session request
  • Tried cache deception with a static-looking suffix on a private URL
  • Captured the response before AND after poisoning to prove the change

Evidence to collect

  • Request with the unkeyed header
  • First (poisoned) response
  • Second, unrelated request showing the same poisoned response served back
  • Cache-related headers on both responses (Age, X-Cache)
  • For deception: proof the private data was retrievable without authentication

Recommended fix

  • Include every input that affects the response in the cache key, or don't cache responses that depend on it
  • Strip/normalize forwarded headers at the edge before they reach the origin
  • Never cache authenticated/personalized responses by default
  • Match cache rules on exact paths, not loose suffix patterns