Open redirects and their real exploitation chains
A redirect feature that trusts a URL from a query parameter looks harmless — the site itself is never actually compromised — but it turns a genuinely trusted domain into a launching point for phishing, which is exactly what makes it valuable to an attacker despite doing no direct damage on its own.
3 min read
The feature: redirecting after login, to wherever the user was headed
@app.route("/login", methods=["POST"])
def login(request):
if authenticate(request):
next_url = request.args.get("next", "/dashboard") # WHERE to send the user after login
return redirect(next_url)A "redirect back to where you came from" feature is genuinely useful and common — a user tries to access a protected page, gets sent to log in, and after logging in successfully, gets redirected back to wherever they originally wanted to go, taken from a next parameter in the URL. Nothing about this specific code checks whether next_url actually points somewhere on the same site.
The exploit: a link that LOOKS trusted, but redirects somewhere malicious
https://real-bank.com/login?next=https://evil-phishing-site.com
A victim receives this link (via email, a message). The DOMAIN in the
visible URL is genuinely real-bank.com — a real, legitimate, trusted
domain the victim recognizes and trusts. They click it, log in normally
(everything up to this point is completely real and legitimate), and
are THEN silently redirected to evil-phishing-site.com — which can be
made to look identical to real-bank.com, prompting for credentials "again"
The exploit's entire value to an attacker is that the link's visible domain is genuinely the real, trusted site — a victim inspecting the URL before clicking sees real-bank.com, a domain they have every reason to trust, and has no easy way to notice that the next parameter will redirect them elsewhere after a real, legitimate login. This is frequently chained into phishing: the attacker's fake page (identical-looking to the real login) captures credentials "one more time," and because the victim just came from a genuinely real login on the genuinely real site, the second, fake prompt doesn't feel suspicious the way an unsolicited login page normally would.
The fix: validate that the redirect target is actually internal
from urllib.parse import urlparse
def is_safe_redirect_url(url, allowed_host):
parsed = urlparse(url)
# A RELATIVE path (no scheme, no host) is always safe — it can only point WITHIN this site
return (parsed.netloc == "" or parsed.netloc == allowed_host) and parsed.scheme in ("", "https")
@app.route("/login", methods=["POST"])
def login(request):
if authenticate(request):
next_url = request.args.get("next", "/dashboard")
if not is_safe_redirect_url(next_url, allowed_host="real-bank.com"):
next_url = "/dashboard" # fall back to a SAFE default, don't trust the parameter
return redirect(next_url)The fix validates that a redirect target is either a relative path (which, by definition, can only point somewhere within the current site) or an absolute URL whose host explicitly matches the application's own domain — anything else falls back to a safe, hardcoded default rather than being trusted. This is the exact same "validate against a known-good shape, don't just accept whatever arrives" principle this domain's earlier lessons kept returning to, applied specifically to redirect destinations.
Why a domain-substring check is a real, common, incomplete fix
# TEMPTING but genuinely broken check:
if "real-bank.com" in next_url: # substring check — feels like it should work
return redirect(next_url)
# BYPASSED by: https://evil.com/real-bank.com (the STRING is present, but
# the actual HOST is evil.com — "real-bank.com" here is just a path segment)
# or: https://real-bank.com.evil.com (a SUBDOMAIN of evil.com, not
# real-bank.com at all, despite literally containing that substring)Checking whether the target URL's string merely contains the expected domain is a real, common mistake — an attacker can construct a URL where the trusted domain name appears somewhere in the string (as a path segment, or as part of a longer subdomain the attacker actually controls) without the URL's actual host being the trusted domain at all. The robust fix has to parse the URL and check the actual host/netloc component specifically, not search the raw string for a substring — the same category of bypass this domain's SSRF lesson covered for blocklist checks generally.
Further reading
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. Why is an open redirect valuable to an attacker for phishing, even though the site itself is never compromised?
2. Why is checking whether a redirect URL's string merely CONTAINS the trusted domain an incomplete fix?
3. Why is a relative path (like /dashboard) always a safe redirect target?