| Platform | Type | Difficulty | Objective |
|---|---|---|---|
| PortSwigger Web Security Academy | Web Cache Deception | Practitioner | Find a delimiter character that the origin server uses to truncate the URL but the cache doesn't recognize, then exploit the discrepancy to cache carlos's /my-account response |
Exploiting path delimiters for web cache deception¶
Logged in as wiener:peter and checked the /my-account response first — no cache headers on it, as expected for dynamic content.
Confirmed static resources like tracking.js do get cached with max-age=30, so the caching layer is definitely active on this target.
First instinct was to reuse the path mapping trick from the previous lab:
GET /my-account/teto.js HTTP/2
Got a 404 Not Found back. This origin doesn't use REST-style abstraction the way the last one did — extra path segments after /my-account aren't silently ignored, they just break the route entirely.
That ruled out path mapping and pointed toward a different discrepancy: I needed a delimiter character that the origin server would use to truncate the path back down to /my-account, while the cache stayed oblivious to it and just saw a path ending in .js.
Sent GET /my-account§§teto.js over to Intruder and loaded the Web Cache Deception delimiter wordlist as the payload set. The one detail that matters here is unchecking "URL-encode these characters" under Payload encoding — otherwise Burp percent-encodes the delimiter before it goes out, and the server never actually sees the raw character being tested.
One character stood out from the rest of the batch — ; came back 200 OK with X-Cache: miss on the first hit, and the response body was the actual account page:
GET /my-account;teto.js HTTP/2
HTTP/2 200 OK
Cache-Control: max-age=30
X-Cache: miss
Content-Length: 3937
(my-account body)
That miss on a 200 carrying dynamic content is exactly the signal to look for — it means the request reached the origin, the origin returned real account data, and the cache went ahead and stored it. A follow-up request to the same URL would flip that to hit, confirming the response is now sitting in cache. The response headers also gave away why ; worked: Server: Apache-Coyote/1.1, which is the Tomcat connector behind Java/Spring. Spring uses ; legitimately as a delimiter for matrix variables, so the origin truncates the path right at the semicolon and serves /my-account as normal. The cache has no idea what a matrix variable is — it just sees a path ending in .js and caches accordingly. Knowing the server stack up front would've let me prioritize that character instead of fuzzing blind.
With the discrepancy nailed down, built the exploit the same way as before:
<script>
document.location="https://0a86009f0385d0f98255fb6800f5005a.web-security-academy.net/my-account;miku.js"
</script>
Delivered it to the victim. Carlos's browser requested /my-account;miku.js on his own session, the origin truncated the path and returned his account page, and the cache stored the whole thing keyed to that .js-terminated URL.
Requested the same URL myself right after:
Got carlos's cached account page back, API key included. Submitted it to close the lab.
Worth noting the difference from the previous lab: since this discrepancy comes from how the server framework itself parses delimiters rather than how one specific endpoint maps paths, it isn't limited to /my-account. If ; works here, it's very likely to work on any other endpoint sitting behind the same origin server — a much broader win than a path mapping bug that's scoped to a single route.
Lab solved and we are almost finishng :P