Open redirect
CWE-601OWASP A01:2021Updated August 31, 20266 min read
An open redirect is a vulnerability in which an application sends visitors to an address taken straight from user input, such as a returnUrl parameter, without checking that the address belongs to its own domain. Attackers use it for phishing under your domain name and to steal OAuth tokens. The fix is an allowlist of relative paths, or a key that the server maps to a fixed path.
Almost every application redirects visitors at some point: back to the page they came from after login, to a confirmation after checkout. Where that destination comes from is usually a parameter named something like returnUrl or next. If the application copies that value without checking it, the person who wrote the link decides where your visitor ends up.
What is an open redirect?
An open redirect is a vulnerability in which an application forwards a visitor to an address supplied entirely by user input, without checking that the address belongs to its own domain. The attacker injects no code and breaks nothing open; they simply borrow the good name of your domain to make someone arrive somewhere else.
Think of a signpost in your lobby that visitors may write on themselves. Whoever reads it trusts it because it stands in your lobby, even when it points at another building. The link the victim receives starts with your domain, and that is exactly what a mail filter, a link scanner and the recipient’s eye check.
In practice the parameter is called returnUrl, next, redirect, url, dest or continue, and it nearly always exists for a good reason: someone who hits a protected page without a session is sent to the login screen with the original page in that parameter. The trouble starts when the parameter is allowed to hold a full URL (Uniform Resource Locator) instead of a path inside your own application.
How does an open redirect attack work?
Take a login route that returns the user to the page named in the parameter after authentication.
Vulnerable:
app.post("/login", async (req, res) => {
const user = await authenticate(req.body.username, req.body.password);
if (!user) return res.status(401).render("login");
req.session.userId = user.id;
// The destination comes straight from the query string
res.redirect(req.query.returnUrl || "/dashboard");
});
The attacker composes a link that starts with your own domain and therefore passes most filters:
GET /login?returnUrl=https://app-example.attacker.test/session HTTP/1.1
Host: app.example.com
The victim logs in with you and is sent on with a 302 to the attacker. Waiting there is a copy of your login screen with a notice that the session has expired. The victim types their password again, this time for somebody else.
The first instinct is a check on the first character: if the value starts with a slash, it must be a path inside the application. That assumption does not hold, because a browser parses an address by more rules than most developers have in mind:
//evil.exampleis not a path but a protocol-relative URL. The browser fills in the scheme of the current page and lands onhttps://evil.example, even though the value does begin with a slash./\evil.exampleand\/evil.exampledo the same thing, because browsers treat a backslash in this part of an address as a slash.https://app.example.com@evil.example/goes toevil.example. Everything before the at sign is user information rather than a host, but to a human the link reads as your domain.https://app.example.com.evil.example/defeats any check that merely looks for your domain name somewhere in the value.
It gets more serious once the redirect sits inside an OAuth flow. An authorization server accepts only redirect_uri values registered in advance, precisely to keep a token from reaching a stranger. If one of those registered addresses hosts an open redirect, that registration is worthless: the authorization code or token is delivered neatly to your domain, and your own redirect hands it on to the attacker.
The safe version lets the user choose a key rather than an address, which the application maps server-side to a path you defined.
Secure:
// Fixed destinations; the user only sends the key
const RETURN_TARGETS = new Map([
["dashboard", "/dashboard"],
["billing", "/account/billing"],
["reports", "/reports/overview"]
]);
app.post("/login", async (req, res) => {
const user = await authenticate(req.body.username, req.body.password);
if (!user) return res.status(401).render("login");
req.session.userId = user.id;
const key = String(req.query.next ?? "");
res.redirect(RETURN_TARGETS.get(key) ?? "/dashboard");
});
If a fixed list is too restrictive, accept relative paths only and validate them with a real URL parser against your own origin:
const BASE = "https://app.example.com";
function safePath(value) {
// Exactly one leading slash, no second slash and no backslash behind it
if (typeof value !== "string" || !/^\/[^/\\]/.test(value)) return "/dashboard";
const target = new URL(value, BASE);
return target.origin === BASE ? target.pathname + target.search : "/dashboard";
}
What is the impact of an open redirect?
On its own the impact stays modest. No data leaks, nothing in your database changes, and the attacker gains no foothold on your systems. What they gain is credibility: a phishing link that starts with the domain of your bank or your portal gets clicked more often, and survives a mail filter more often, than a link to an unknown address. In that form the rating is low.
The severity moves to medium as soon as the redirect sits in a chain. In an OAuth flow it can carry an authorization code or an access token off-site and lead to full account takeover. The same applies to single-use links from a password reset mail and to tokens that ride along in the Referer header to the next host. A variant that points not at an external host but at javascript: or data:, executed on the client, additionally yields cross-site scripting. In business terms that means reputational damage and, at worst, your domain on a browser or mail provider blocklist.
How do you detect an open redirect?
Start by inventorying every place where the application takes a destination from input. Search the code for redirect, for anything that sets the Location header and, on the client side, for assignments to location.href, location.replace and meta refresh tags. Then search traffic and logs for parameter names such as returnUrl, next, url and dest, in POST bodies as well.
Testing means replacing the value with a domain you control and fetching the response without following the redirect, for example with curl -i, so you see the Location header directly. If the value is rejected, work through the bypasses above: the double slash, the backslash variants, the at sign, and your domain name used as a subdomain. Do not skip the client-side cases, where the redirect is executed in JavaScript.
Scanners report the textbook case, but generate a lot of noise and miss redirects behind a login screen or halfway through a multi-step process. AssistSec covers open redirects in a penetration test and examines the chain as well: whether the redirect can be used to pull a token or a single-use link off-site.
How do you prevent an open redirect?
- Pass keys, not addresses. Have the user send a short identifier that the server maps to a fixed path. Anything not on your list goes to the default destination.
- Accept relative paths only. Reject every value carrying a scheme, a host or an at sign, and normalise the result before it reaches the
Locationheader. - Explicitly reject a double slash and any backslash. These are the two bypasses that survive a check on the first character in almost every browser.
- Validate with a URL parser, never with a substring check. Compare the full origin, meaning scheme, host and port together.
- Treat
redirect_uri, reset links and SSO return points as their own case. Match them exactly against a registered value and allow no wildcards or free path suffixes. - Show an interstitial when an external redirect is genuinely needed. Display the destination and let the visitor confirm it.
Sources
- CWE-601: URL Redirection to Untrusted Site (Open Redirect)cwe.mitre.org
- OWASP Cheat Sheet: Unvalidated Redirects and Forwardscheatsheetseries.owasp.org
- PortSwigger Web Security Academy: OAuth 2.0 authentication vulnerabilitiesportswigger.net
- RFC 9700: Best Current Practice for OAuth 2.0 Securitydatatracker.ietf.org
Frequently asked questions
Is an open redirect really a vulnerability?
Yes, although on its own the severity is limited. No data leaks, nothing in your database changes, and the attacker gains no access to your systems; what they borrow is the reputation of your domain. As soon as the redirect becomes part of a chain, in an OAuth flow or on a password reset link, the severity rises. Treat it as a real defect rather than a cosmetic remark.
Why does //evil.example work when my check tests for a leading slash?
Because two leading slashes are not a path but a protocol-relative URL. The browser fills in the scheme of the current page and ends up at https://evil.example. The value does start with a slash, so a check on the first character lets it through. The same trick works with a backslash, because browsers treat it as a slash in that part of the URL.
What does an open redirect have to do with OAuth?
An authorization server only accepts redirect_uri values that were registered in advance. If one of those registered addresses contains an open redirect, that registration stops protecting anything: the authorization code or token is delivered to your domain first and then forwarded to the attacker by your own redirect. This is why the OAuth security guidance explicitly requires clients not to host open redirects.
How do I keep returnUrl functionality without the vulnerability?
You do not have to drop the feature, only narrow what the input may say. Let the user send a key that the server maps to a fixed path, or accept relative paths only and validate them with a real URL parser against your own origin. Both options keep the return behaviour intact and rule out external destinations by design.
Does a Content Security Policy stop an open redirect?
No. A Content Security Policy restricts which resources a page may load and execute, but an open redirect is an ordinary HTTP response with status 302 and a Location header. The browser follows that redirect before any page carrying a policy is rendered. Only server-side validation of the destination helps.
Related articles
- GlossaryPentestA penetration test (pentest) is a controlled attack on your systems by ethical hackers. Learn how a pentest works and what vulnerabilities it uncovers.
- VulnerabilitiesCWE-284A01:2021Broken access controlBroken access control explained: horizontal and vertical privilege escalation, forced browsing, and how deny by default fixes it server-side.
- VulnerabilitiesCWE-79A03:2021Cross-site scripting (XSS)Cross-site scripting (XSS) lets attackers inject malicious scripts into web pages that run in visitors' browsers. Learn how XSS works and how to prevent it.
- VulnerabilitiesCWE-644A03:2021Host header injectionIf your application builds links from the Host header, the attacker decides where they point. Learn how that hijacks password resets.
- VulnerabilitiesCWE-918A10:2021Server-side request forgery (SSRF)Server-side request forgery (SSRF) explained: how attackers abuse your server to reach internal systems and cloud services, and how to prevent it.