07.06 · OWASP A01:2021 (Broken Access Control)

Open Redirect

A login page that takes a ?next= parameter so it can send you back where you came from after auth. Easy to write. Easy to weaponize. The phishing email looks like it came from your bank because it did.

The shape of the bug

A URL parameter controls where the application sends the user next. The application redirects to whatever the parameter says.

The vulnerable patternvulnerable
# Flask, Django, Express, Rails, .NET MVC — the pattern repeats @app.route("/login") def login(): next_url = request.args.get("next", "/") if user_authenticated(): return redirect(next_url) # attacker controls next_url

Now the attacker crafts:

https://bank.com/login?next=https://bank-secure-login.attacker.com/

The user clicks this link — from a phishing email, from a malicious tweet, from a comment posted somewhere. They see bank.com in the URL, log in successfully, and the bank redirects them to bank-secure-login.attacker.com. The attacker's site looks identical to bank.com's account page. The user enters their second-factor code. Game over.

The attacker never compromised the bank. They just used the bank as a redirect amplifier.

Where it hides

  • Login flows: ?next=, ?return_to=, ?redirect=, ?back=, ?continue=, ?destination=.
  • Logout flows: "after logout, return to..."
  • Password reset: the link in the email contains a return URL.
  • OAuth and SAML callback URLs: the redirect_uri parameter. A separate, related bug class — see the OAuth redirect attack lab.
  • Marketing tracking links: https://example.com/track?u=https://destination.com. Common in email campaigns. Attacker uses your tracking link to wrap a malicious destination.
  • HTML <meta http-equiv="refresh"> with attacker-controlled URL.
  • JavaScript window.location = userInput — client-side open redirect, often via URL fragment.

Bypass tricks

Developers add naive checks. Attackers route around them.

Naive checkBypass
startsWith("/")//evil.com/path — protocol-relative URL. Browser treats as https://evil.com/path.
contains("yoursite.com")https://evil.com/login?fake=yoursite.com or https://yoursite.com.evil.com.
endsWith("yoursite.com")https://evil.com/yoursite.com.
Blocklist of known bad domainsUse any domain not on the list. There are infinitely many.
Allow only yoursite.com subdomainsSubdomain takeover — if you have an abandoned DNS record pointing at S3/Heroku/etc and the attacker can claim it, they get a *.yoursite.com redirect target.
startsWith("https://yoursite.com")https://yoursite.com.evil.com — the check passes because yoursite.com.evil.com starts with the right string.

The right way

Pattern 1: allow only relative paths

Often the entire feature is just "send me back to the page I was on." That page is always on your site. So accept only relative paths starting with /, and refuse anything containing ://, //, or scheme-like characters.

Python · relative-onlysafe
from urllib.parse import urlparse def safe_redirect(url, default="/"): parsed = urlparse(url) # reject if any of: scheme, netloc, leading // (protocol-relative) if parsed.scheme or parsed.netloc: return default if not url.startswith("/") or url.startswith("//"): return default return url

Pattern 2: allow-list of full URLs

If the feature requires redirecting to specific external partners, maintain an allow-list. Compare the parsed hostname exactly — not a substring match.

Allow-list with exact host matchsafe
ALLOWED_HOSTS = {"partner1.com", "partner2.com"} def safe_redirect(url, default="/"): parsed = urlparse(url) if parsed.hostname in ALLOWED_HOSTS and parsed.scheme == "https": return url return default

Pattern 3: indirection token

Sometimes you need to support arbitrary redirect destinations (marketing tracking links, search results). Don't redirect from a URL parameter. Instead, store the destination server-side and let the user pass a token that looks it up.

# /track/abc123 -> server looks up destination for token abc123, redirects there # The attacker can't put their domain in the URL because the URL doesn't contain one.

Why open redirect matters

By itself, open redirect causes no direct technical damage — it doesn't read data, doesn't execute code, doesn't break authentication. CVSS scores it low. Many bug bounty programs explicitly mark it as out of scope.

But it is the single most effective accelerator for phishing campaigns. When the phishing URL starts with https://your-bank.com/login?, the user's mental model says "this is my bank." Email security filters that check the URL domain see your bank's domain and let it through. URL preview features in chat apps show your bank's title and favicon. The attacker has borrowed your reputation.

In combination with other bugs, it gets worse:

  • OAuth flows: open redirect on the callback URL lets the attacker steal authorization codes (see the redirect_uri attack).
  • Token-bearing redirects: if your redirect URL includes a session token or password reset token in the query string, the attacker's server captures it directly via Referer header.
  • SSO chains: open redirects in one app become login-bypass tools for federated apps that trust it.

The takeaway

Open redirect is the politest serious vulnerability. The application "just does what you ask." But "what you ask" can be a phishing trampoline, an OAuth token thief, or a reputation laundering service.

The fix is structural: don't pass full URLs in redirect parameters. Pass relative paths only, or use an allow-list, or use server-side indirection tokens. Every login page, every password reset, every OAuth callback, every logout flow — audit each one.

References

Formatted in APA 7.

  1. OWASP. (2024). Unvalidated redirects and forwards cheat sheet. OWASP Cheat Sheet Series. https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html
  2. MITRE. (2024). CWE-601: URL redirection to untrusted site ('open redirect'). Common Weakness Enumeration. https://cwe.mitre.org/data/definitions/601.html
  3. PortSwigger. (2024). Open redirection vulnerabilities. Web Security Academy. https://portswigger.net/kb/issues/00500100_open-redirection-reflected