The reliable open redirect vulnerability fix is to stop redirecting to attacker-controlled URLs at all: validate every redirect target against an allowlist of your own paths or hosts, and fall back to a safe default when validation fails. Everything else in this post is detail around that one rule. Open redirects (CWE-601) look harmless next to injection bugs, which is exactly why they survive code review, and why they show up so often in phishing campaigns and OAuth token theft.
What is open redirect?
If you are asking what is open redirect, here is the short version: an application takes a URL from user input (a query parameter, a form field, a header) and sends the browser there with an HTTP redirect, without checking where the URL points.
The classic pattern is a login flow:
https://app.example.com/login?next=/dashboard
After authentication, the app reads next and issues a 302 to it. If nothing validates that value, this also works:
https://app.example.com/login?next=https://evil.example.net/fake-login
The victim clicks a link that genuinely starts on your domain, passes your real login page, and lands on an attacker page styled to look like yours. The address bar told the truth for the first hop, which is all most users check.
Common places these bugs live:
?next=,?return_to=,?redirect_uri=,?url=,?goto=parameters- Logout endpoints that bounce users back to a "came from" page
- Email click-tracking and campaign link wrappers
- OAuth and SSO callback handling
- Language or region switchers that reload "the same page"
Why a boring bug is worth fixing
On its own, an open redirect does not read your database or execute code. Its value to an attacker is laundering trust:
- Phishing that survives link inspection. A link to
login.yourbank.com/...passes casual scrutiny, email security gateways that reputation-score the visible domain, and sometimes even user training. - OAuth and SSO token leakage. If an authorization server or client is loose about
redirect_urimatching, an open redirect on an allowed domain can be chained to leak authorization codes or tokens to an attacker host. Several real-world account-takeover chains have been built on exactly this: the open redirect was the weakest, cheapest link. - Bypassing allowlists elsewhere. Server-side request forgery filters, CSP
form-actionrules, and "trusted domain" checks in other systems can sometimes be bounced through a redirect on a trusted host.
That chaining behavior is why bug bounty programs still pay for open redirects despite triaging them as low severity in isolation.
Finding open redirects in your own app
Three approaches, in increasing order of effort:
Grep for redirect sinks. Search your codebase for the redirect primitives of your stack and trace where the target comes from:
rg -n "redirect\(|sendRedirect|Location:|RedirectResponse|res\.redirect" --type-add 'src:*.{py,js,ts,java,rb,php,go}' -tsrc
Any hit where the argument originates from request data without validation is a candidate.
Test the parameters directly. For each redirect-looking parameter, try payload variants, because naive fixes block only the obvious one:
?next=https://attacker.example
?next=//attacker.example
?next=/\attacker.example
?next=https:attacker.example
?next=https://yourapp.com.attacker.example
?next=https://attacker.example%2F%2E%2E
The //host form is the one most homegrown validators miss: it is a scheme-relative URL, so redirect("//attacker.example") leaves your site even though the string "looks like a path."
Scan for it. Open redirect checks are standard in DAST scanners; a crawler that fuzzes query parameters and watches Location headers will flag these without you enumerating endpoints by hand. A DAST scanner run against staging on every release is the cheapest regression net, because redirect parameters tend to get added by feature work, not by security-aware code.
The fix that holds: allowlist, not blocklist
Blocklisting known-bad patterns fails repeatedly against URL parser quirks. The durable fixes, in order of preference:
1. Do not accept URLs at all. If the legitimate use case is "return to a page on our site," store the target server-side (session key, signed token, or a numeric route ID) and pass an identifier through the flow instead of a URL.
2. Accept only relative paths, strictly. If the parameter must carry a location, require it to be an internal path:
from urllib.parse import urlparse
def safe_next(raw: str, default: str = "/dashboard") -> str:
if not raw:
return default
# Reject absolute and scheme-relative URLs, and backslash tricks
if raw.startswith(("//", "/\\")) or "\\" in raw:
return default
parsed = urlparse(raw)
if parsed.scheme or parsed.netloc:
return default
if not raw.startswith("/"):
return default
return raw
Note the explicit checks before parsing: urlparse("//evil.example") yields an empty scheme but a netloc, and some frameworks normalize backslashes to forward slashes after your check runs. Validate the raw string and the parsed result.
3. Allowlist full hosts when you must redirect off-site. Compare the parsed hostname against an exact set, never with startswith or in:
const ALLOWED_HOSTS = new Set(["app.example.com", "docs.example.com"]);
function safeRedirect(raw, fallback = "/") {
let url;
try {
url = new URL(raw, "https://app.example.com");
} catch {
return fallback;
}
if (!["https:"].includes(url.protocol)) return fallback;
if (!ALLOWED_HOSTS.has(url.hostname)) return fallback;
return url.toString();
}
Substring checks are the classic mistake: url.includes("example.com") approves example.com.attacker.net, and hostname.endsWith("example.com") approves notexample.com unless you also check the preceding dot. Parsing rules for JavaScript specifically are worth their own read; we cover the parser-versus-regex tradeoffs in URL validation in JavaScript.
Framework-native helpers
Most mature frameworks already ship a vetted validator. Use it instead of writing your own:
- Django:
django.utils.http.url_has_allowed_host_and_scheme(url, allowed_hosts=...), whichLoginViewuses fornexthandling. - Rails:
redirect_toraises on external hosts by default since Rails 7 unless you passallow_other_host: true. - Spring Security: configure a strict
AuthenticationSuccessHandler; avoid echoing a rawredirectparameter intosendRedirect. - Express: nothing built in, so wrap
res.redirectbehind a helper like the one above and lint against direct calls.
For OAuth specifically, register exact redirect_uri values and compare with strict string equality on the authorization server. Wildcard or prefix matching of redirect URIs is how open redirects escalate into token theft.
Keeping it fixed
Open redirects regress because new redirect parameters keep appearing. Three cheap controls:
- A unit test suite that feeds the payload list above into every redirect helper.
- A lint or code-review rule flagging direct calls to raw redirect APIs outside the helper.
- Recurring dynamic scanning of staging, since scanners exercise the app the way an attacker would rather than the way your tests imagine. Pairing that with dependency scanning matters too, because redirect bugs also arrive inside third-party middleware; an SCA tool such as Safeguard will flag a vulnerable redirect-handling package even when your own code is clean.
If your team wants a structured walkthrough of redirect handling and the OAuth failure modes, the exercises in our security academy include a lab for exactly this class.
FAQ
Is an open redirect really a vulnerability if no data is exposed?
Yes. Its impact is indirect: credible phishing, OAuth code leakage, and filter bypasses. Severity is context-dependent, which is why CVSS scores for CWE-601 issues range widely, but "low severity" is not "no severity."
What is the fastest open redirect vulnerability fix for a legacy app?
Centralize: route every redirect through one helper that enforces relative-paths-only with a safe fallback, then chase down the few legitimate cross-host cases and put them on an exact-match allowlist.
Does a Content Security Policy stop open redirects?
No. CSP governs what a page may load or submit to, not where an HTTP 302 may send the browser. Server-side validation is the only control at the redirect itself.
How do scanners detect open redirects?
They inject marker URLs into likely parameters and check whether the response's Location header (or a meta refresh / JavaScript navigation) points at the injected external host. That is why parameters only reachable after login need an authenticated scan configuration.