This article explains web cache poisoning through a practical lab exercise where attackers exploit the gap between what a cache uses as its key and what influences the actual response. CacheServ demonstrates how an unkeyed input—specifically the X-Forwarded-Host header—can be reflected into HTML without being part of the cache key, allowing a single poisoned request to be stored and served to all subsequent visitors requesting the same URL. This vulnerability matters because it converts a reflected quirk into a persistent, mass attack affecting every user who shares the cached key.
- Cache key mismatch enables broad poisoningCacheServ caches pages by URL only but reflects the X-Forwarded-Host header into the HTML response. Because this header is not included in the cache key, an attacker can send one request with a malicious header value, which gets cached under the normal URL key. All subsequent visitors requesting that URL receive the poisoned cached content, even though they never sent the malicious header themselves.
- Unkeyed inputs create the exploit windowAn unkeyed input is any request element—typically a header like X-Forwarded-Host—that changes the response but is excluded from the cache key. The article identifies this mismatch as 'the whole bug': attackers exploit it by confirming a header is reflected but unkeyed, sending a poisoned request that gets cached, then verifying a clean request serves the poisoned content to simulate victim exposure.
- Attack requires three discrete stepsThe attack proceeds by confirming X-Forwarded-Host is reflected but unkeyed, sending a request to the target URL with a malicious header value to poison the cache, and finally making a clean request without the header to confirm the cached poisoned response is served. This third step is crucial because it simulates what a victim would receive and demonstrates the attack's scope.
- Defense requires input inclusion or exclusionThe article presents two defensive strategies: either include every input that influences the response in the cache key, or strip those inputs before caching occurs. Additionally, it recommends not reflecting request headers into responses unless absolutely necessary and never doing so without validation; headers should be allowlisted and normalized at the edge to prevent unexpected ones from reaching the application.
- Real-world discovery and classificationJames Kettle's 2018 research 'Practical Web Cache Poisoning' demonstrated unkeyed-header attacks against major sites and frameworks, making this technique widely known. The vulnerability maps to OWASP A05 (Security Misconfiguration) in the 2021 Top 10, establishing its classification as a serious misconfiguration rather than an isolated quirk.
- Cache scope and response composition matterThe article advises scoping cache entries carefully and keeping cacheable responses free of request-specific data. This principle acknowledges that even properly keyed caches can be vulnerable if responses retain user-specific or request-specific content that should not be shared across different clients.
- Practical testing approach for researchersTo hunt cache poisoning on authorized targets, the article recommends adding headers like X-Forwarded-Host and observing whether they are reflected in responses. If they are reflected and not part of the cache key, a single request can affect multiple users, indicating a vulnerability worth deeper investigation and testing.
Bottom line: Web cache poisoning succeeds when a request header or input changes the cached response but is excluded from the cache key; defending against it requires either including all response-affecting inputs in the cache key or stripping them before caching, combined with validation and careful scope management. Attackers should test for reflected unkeyed headers during authorized assessments, and defenders must ensure every element that influences a response is either keyed or never reflected unvalida
X-Forwarded-Host header into the HTML — but that header is not part of the cache key. So one poisoned request can be stored and then handed to every visitor who asks for the same URL.📋 In this walkthrough
What this lab teaches
Web cache poisoning abuses the gap between what a cache uses as its key and what actually influences the response. If a header changes the page but is not part of the cache key (an 'unkeyed input'), an attacker can send one request that both injects content and gets cached — then every later visitor to that URL is served the attacker's version.
The lesson: anything that affects the response must be part of the cache key, or it becomes a way to poison the cache for everyone.
How CacheServ keys its cache
CacheServ builds the cache key from the URL only, but reflects the X-Forwarded-Host header into the page. Send a request with a malicious value in that header and the poisoned response gets stored under the normal URL key — so the next visitor, sending no such header, still receives your content.
The attack approach
- Confirm X-Forwarded-Host is reflected into the HTML but is not part of the cache key.
- Send a request to the target URL with a malicious X-Forwarded-Host value.
- Get that poisoned response cached under the ordinary URL key.
- Make a clean request (no special header) and confirm the cache serves the poisoned content.
Full walkthrough (spoiler)
If the clean request comes back untouched, your poisoned response did not get cached — open the steps.
Show the step-by-step solution
- Send a request to the cached URL with a crafted X-Forwarded-Host header that the page will reflect.
- Because the header is unkeyed, the cache stores your poisoned response under the plain URL key.
- Send a second, normal request to the same URL without the header — simulating a victim.
- The cache serves the stored poisoned content to that request. The lab detects the victim receiving your injected content and marks you solved.
Why it works
The cache assumes the URL fully determines the response, so it reuses one stored copy for every visitor. But the reflected header also shapes the page and is not in the key. One attacker request fixes the stored copy; everyone who shares that key then gets it. The mismatch between 'what affects the response' and 'what is keyed' is the whole bug.
Where this happens in the real world
James Kettle's 2018 research 'Practical Web Cache Poisoning' demonstrated this class against major sites and frameworks, popularising unkeyed-header attacks via headers like X-Forwarded-Host. It maps to Security Misconfiguration (A05) in the OWASP Top 10 (2021) and can turn a reflected quirk into a mass, persistent attack.
How to defend against it
- Key the cache on every input that influences the response, or strip those inputs before caching.
- Do not reflect request headers like X-Forwarded-Host into responses unless you must, and never unvalidated.
- Allowlist and normalise headers at the edge so unexpected ones cannot reach the app.
- Scope cache entries carefully and keep cacheable responses free of request-specific data.
Try it, then go further
Reading about it is the easy part. Open the lab, try your own approach before peeking at the spoiler, and see how many attempts it takes.
