June 16, 2026
Understanding Web Cache Poisoning via Unkeyed Headers: A PortSwigger Lab Walkthrough
Imagine being able to send a single HTTP request and have every visitor to a website execute attacker-controlled JavaScript.
ThatRandomGuy (Likhith Raj V)
5 min read
This is exactly what web cache poisoning makes possible.
In this article, we'll understand how web cache poisoning works, why unkeyed headers are dangerous, and walk through a PortSwigger Web Security Academy lab to exploit the vulnerability using Burp Suite Community Edition.
What Is Web Cache Poisoning?
Modern websites often rely on caching mechanisms, often provided by CDNs and reverse proxies, to improve performance. Instead of generating a page for every request, the server can reuse previously generated responses and deliver them directly from cache.
This significantly reduces response times and server load.
However, problems arise when user-controlled input affects the response, but the caching layer ignores that input when deciding whether a cached response already exists.
As a result, an attacker may be able to store a malicious response in the cache and have it served to other users. This vulnerability is known as web cache poisoning.
Understanding Cache Keys
Whenever a request reaches a cache, the cache must determine whether it already has a stored response for that request.
To do this, it generates a cache key.
A cache key commonly includes:
- URL path
- Query parameters
- Host name
Depending on the caching mechanism and configuration, additional attributes may also be included.
If two requests produce the same cache key, the cache assumes they should receive the same response.
The problem occurs when the application considers certain inputs important, but the cache does not.
Why Unkeyed Headers Matter
Suppose an application uses the value of an HTTP header when generating a page.
If the caching layer ignores that same header while creating the cache key, a mismatch occurs.
The server thinks the header matters, but the cache does not.
As a result, a response generated using attacker-controlled input can be stored in the cache and later served to completely unrelated users.
This discrepancy between application behavior and cache behavior is what makes web cache poisoning possible.
PortSwigger Lab Overview
The PortSwigger Web Security Academy provides several labs that demonstrate Web Cache Poisoning in a controlled environment. In this article, we will solve the lab that exploits unkeyed headers.
For this walkthrough, I used Burp Suite Community Edition. Although it lacks some features available in the Professional edition, it is more than sufficient for learning and understanding the fundamentals of web cache poisoning.
Setting Up Burp Suite
First, launch Burp Suite and open the lab. Before accessing the lab page, navigate to Proxy → Intercept and ensure that interception is turned on.
Now load the lab page in your browser. Burp Suite intercepts the request to the homepage (/). In HTTP terminology, this intercepted message is simply called an HTTP request.
Right-click the request and choose Send to Repeater. Once the request has been sent to Repeater, you may turn interception off so that the browser can continue functioning normally.
Observing Cache Behaviour
Inside Repeater, send the request and observe the response. On the first request, you will typically see a cache miss, indicating that the response has been generated by the server rather than served from cache.
The response also contains a cache lifetime of approximately 30 seconds, meaning that the cached content remains valid during that interval.
Using a Cache Buster
To better understand the cache behaviour, modify the request line:
GET / HTTP/2GET / HTTP/2to something like:
GET /?cb=jason1234 HTTP/2GET /?cb=jason1234 HTTP/2The parameter cb=jason1234 acts as a cache buster. Sending the request for the first time produces another cache miss. Sending it again results in a cache hit.
This happens because the caching mechanism stores responses based on several components, including (but not limited to):
- Request method
- URI path
- Query string
- Host header
- Additional cache-specific attributes
Together, these components form the cache key, which determines whether a previously generated response can be reused.
Testing the X-Forwarded-Host Header
Next, remove the cache buster and add the following header:
X-Forwarded-Host: example.comX-Forwarded-Host: example.comThe X-Forwarded-Host header is commonly used by reverse proxies to indicate the original host requested by the client. Some applications use this value when generating URLs or including external resources.
Send the request repeatedly until a cache hit occurs.
Interestingly, we now observe the following script tag inside the response body:
<script type="text/javascript" src="//example.com/resources/js/tracking.js"></script><script type="text/javascript" src="//example.com/resources/js/tracking.js"></script>This is a valuable finding because the application is using the value supplied in the X-Forwarded-Host header to construct the script URL.
However, the cache does not include this header in its cache key. Therefore, once this modified response is cached, every user requesting the same page receives the manipulated response.
This mismatch between the application's behaviour and the cache's behaviour is the root cause of the vulnerability.
Leveraging the Exploit Server
Returning to the lab page, click Exploit Server.
The exploit server allows us to host attacker-controlled content. We modify the response body on the exploit server so that it contains:
alert(document.cookie)alert(document.cookie)After storing the exploit by clicking on store in the bottom, copy the exploit server domain (excluding the protocol), which looks similar to:
exploit-id.exploit-server.net/exploitexploit-id.exploit-server.net/exploitNow replace the previous header value:
X-Forwarded-Host: exploit-id.exploit-server.net/exploitX-Forwarded-Host: exploit-id.exploit-server.net/exploitSend the request repeatedly until a cache hit is obtained.
We see a poisoned script in the body of the response:
<script type="text/javascript" src="//exploit-id.exploit-server.net/exploit/resources/js/tracking.js"></script><script type="text/javascript" src="//exploit-id.exploit-server.net/exploit/resources/js/tracking.js"></script>Once the poisoned response is cached, refreshing the lab page causes browsers to load the malicious JavaScript file from the exploit server. As a result, the JavaScript executes, showing us the cookie as a pop up, and the lab is solved.
Understanding What Happened
The attack succeeds because:
- The application trusts the
X-Forwarded-Hostheader when generating the script URL. - The cache ignores this header while creating the cache key.
- A malicious response becomes cached.
- Other users receive the poisoned response from the cache server.
- Their browsers load and execute attacker-controlled JavaScript.
Thus, a single manipulated request can affect multiple users, demonstrating why web cache poisoning poses a significant threat to the integrity of cached content.
Discovering Unkeyed Headers with Param Miner
An excellent extension for identifying these issues is Param Miner, available from the BApp Store under the Extensions tab.
After installing the extension, right-click a request inside Repeater and select:
Extensions → Param Miner → Guess HeadersExtensions → Param Miner → Guess HeadersKeeping the default settings is usually sufficient.
Once the scan completes, navigate to:
Extensions → Param Miner → OutputExtensions → Param Miner → OutputParam Miner highlights interesting headers and hidden inputs that may influence server responses but are not included in the cache key.
These findings provide valuable clues when hunting for:
- Unkeyed headers
- Cache poisoning vectors
- Hidden parameters
- Request smuggling opportunities
Users of Burp Suite Professional can also view many of these findings directly within the Target section.
Param Miner is therefore an extremely useful tool for identifying potential web cache poisoning vulnerabilities and gaining insight into how applications process hidden inputs.
Why Is Web Cache Poisoning Dangerous?
Successful cache poisoning can lead to:
- Cross-site scripting (XSS)
- Defacement of pages
- Redirection to malicious websites
- Delivery of attacker-controlled JavaScript
- Simultaneous impact on many users
Because cached responses are shared, a single malicious request can affect a large number of visitors.
How Developers Can Prevent It
Developers can reduce the risk of web cache poisoning by:
- Avoid unnecessary reflection of header values.
- Including all response-influencing inputs in the cache key.
- Disabling caching for highly dynamic content when appropriate.
- Validating and sanitizing user-controlled input.
- Regularly testing applications using Burp Suite and PortSwigger Web Security Academy labs.
Conclusion
Web cache poisoning demonstrates how security issues often arise not from a single component, but from inconsistencies between multiple components.
The application trusts certain inputs when generating responses, while the cache ignores those same inputs when deciding whether a response already exists.
This disagreement creates opportunities for attackers to poison shared content and potentially impact many users with a single request.
Understanding these subtle mismatches is essential for both security researchers and developers, and PortSwigger's labs provide an excellent environment for exploring these concepts safely and responsibly.