An open redirect rarely shows up on its own in a severity report, so it gets triaged as low and forgotten. Attackers do not triage it that way. A link that starts with your trusted domain and quietly lands on theirs is one of the most effective phishing primitives there is — and when the redirect sits inside an OAuth flow, it becomes a route to full account takeover.
What is an open redirect?
An open redirect (CWE-601, URL Redirection to Untrusted Site) is a vulnerability where an application takes a user-controlled destination and redirects the browser to it without validating that the target is trusted. Because the initial URL points at your legitimate domain, victims and security filters extend trust to it — then the browser is forwarded to an attacker-controlled site. The flaw's real damage is almost always downstream: credential phishing and the theft of OAuth authorization codes or tokens.
How the attack works
The typical entry point is a "return to" or "next" parameter used after login or a workflow step:
https://app.example/login?next=https://evil.tld/fake-login
A user who sees app.example at the start of the URL trusts it. After authenticating, the app forwards them to evil.tld, which serves a pixel-perfect clone of your login page and harvests their credentials. Attackers dress the payload up with encoding, protocol-relative URLs (//evil.tld, which browsers treat as absolute), embedded credentials (https://app.example@evil.tld), and backslash tricks that some parsers mishandle.
The higher-severity variant lives in OAuth 2.0. If an authorization server does not strictly validate redirect_uri against pre-registered values, an attacker can point the flow at their own endpoint and capture the authorization code or access token for the victim's session. This is why the OAuth 2.0 Security Best Current Practice mandates exact-match redirect URI validation rather than pattern or prefix matching.
Vulnerable vs. fixed
A login handler that redirects to whatever next contains:
# VULNERABLE — redirects to any user-supplied URL
from flask import request, redirect
@app.get("/login/callback")
def callback():
dest = request.args.get("next", "/")
return redirect(dest) # next=https://evil.tld -> off-site redirect
The fix validates the destination against an allow-list of trusted hosts, and defaults to relative-only redirects so an absolute off-site URL is simply rejected:
# FIXED — allow only same-site relative paths (or an explicit host allow-list)
from urllib.parse import urlparse
from flask import request, redirect
ALLOWED_HOSTS = {"app.example"}
def safe_redirect_target(dest: str) -> str:
parsed = urlparse(dest)
# Reject absolute URLs, protocol-relative (//evil), and backslash tricks.
if parsed.scheme or parsed.netloc or dest.startswith(("//", "/\\", "\\")):
# If you must allow some external hosts, check parsed.netloc here.
if parsed.netloc not in ALLOWED_HOSTS:
return "/"
return dest if dest.startswith("/") else "/"
@app.get("/login/callback")
def callback():
return redirect(safe_redirect_target(request.args.get("next", "/")))
The safest design accepts only relative paths for redirect parameters. When an external destination is genuinely required, validate the parsed host against an explicit allow-list — never with a startswith/substring check, which evil.tld/app.example or app.example.evil.tld defeats.
Prevention checklist
- Prefer relative paths. If a redirect target only ever needs to stay on your site, reject anything with a scheme or host outright.
- Allow-list destinations by exact host when external redirects are required. Do not use prefix or substring matching.
- Neutralize evasions. Account for protocol-relative
//, backslashes, embedded credentials (@), and encoded variants during validation. - Use indirection. Map a short, server-side token or ID to the real destination instead of putting the URL in the parameter.
- Enforce exact-match
redirect_uriin OAuth. Register full callback URLs and reject anything that is not an exact match — this closes the token-theft variant. - Warn on external navigation. An interstitial "you are leaving app.example" page reduces the phishing value of any redirect that does slip through.
- Do not reflect unvalidated URLs into
Locationheaders,meta refresh, or client-sidewindow.location.
Why "low severity" is the wrong instinct
Open redirect gets down-ranked because, in isolation, it "only" sends a browser elsewhere. Its value to an attacker is as a force multiplier. Chained with a phishing campaign, it lends your brand's credibility to a fake login page and slips past link filters that trust your domain. Chained with an OAuth flow, it can leak an authorization code or token and escalate to account takeover. Chained with a filter gap, a javascript: redirect can even become script execution in some contexts. Bug bounty programs routinely pay for open redirects precisely because attackers pair them with these higher-impact primitives. Rate the bug by what it enables, not by the single redirect it performs on its own.
How Safeguard helps
Open redirects are easy to introduce and easy to miss in review because the vulnerable code looks like ordinary navigation. Griffin AI code review traces user-controlled values into redirect sinks — redirect(), Location headers, res.redirect, window.location — and flags targets that are not constrained to an allow-list or relative paths, including the OAuth redirect_uri handling that carries the worst impact. Safeguard's DAST engine probes redirect parameters with off-site and protocol-relative payloads to confirm which ones actually forward the browser to an external host at runtime. When the fix is adding host validation or tightening a redirect helper, Safeguard's auto-fix prepares the pull request, and the Safeguard CLI catches the issue during development. Redirect-handling flaws that originate in a framework or auth library are tracked by Safeguard's software composition analysis.
Curious how a runtime-aware platform compares to a traditional scanner? Read Safeguard vs Snyk.
Start a free scan at app.safeguard.sh/register, and find redirect and OAuth hardening guides at docs.safeguard.sh.