Open redirect vulnerability: examples, CVEs, safe code and test cases
An open redirect (open redirection) is a flaw where an application sends the browser to a URL taken from user input without checking it. Attackers use it to make phishing links look trusted and to leak OAuth tokens. Fix it by redirecting only to local paths, an allowlist or a server-side ID map. On its own it is usually a low finding.
An open redirect (open redirection) is a flaw where an application sends the browser to a URL that comes from user input without checking where it points. Attackers use it to give a phishing link your trusted domain, and to leak OAuth codes and tokens. CWE-601 names the weakness. This page shows the vulnerable line, how attackers chain it, the inputs a naive check misses, four CVEs, safe code for seven stacks and the evidence a tester records.
Here is the vulnerable pattern in Express. It redirects to whatever the next parameter holds:
app.get("/login/done", (req, res) => res.redirect(req.query.next));
How does an open redirect work?#
The application builds a redirect from a request value. The server sends a 302 response with a Location header, or client-side code sets window.location, and the browser goes where the value says. The link the victim sees starts with your domain, so it looks safe.
Illustrative request: Harbor Notes
This is a demonstration. No real customer system was tested. Harbor Notes is a fictional note app and example.com is a reserved example domain. attacker.test is a reserved test domain.
A login flow at https://app.example.com/login?next=/notes sends the user to /notes after sign-in. A tester with written authorization changes next to an external host they control:
GET /login/done?next=https://attacker.test/landing HTTP/1.1
Host: app.example.com
HTTP/1.1 302 Found
Location: https://attacker.test/landing
The Location header holds the external URL, so the application redirects off-site on request. The evidence for the report is the full request URL, the status and the Location value. The tester stops here. No further action against a real user is needed to prove the flaw.
Microsoft's ASP.NET Core documentation describes the standard login version: an attacker sends a link to your login page with a returnUrl that points to a look-alike site, the user signs in, and the app sends them to the look-alike, where a second login form collects the credentials. The user thinks the first sign-in failed.
How do attackers abuse an open redirect?#
Open redirects matter because of what they enable. The common chains are below.
| Chain | What the redirect gives the attacker | Source |
|---|---|---|
| Phishing | A link that starts with the real domain, so users and mail filters trust it | Microsoft Learn |
| OAuth code or token leak | A trusted redirect target that forwards the code or token to another host | RFC 9700 |
| SSRF filter bypass | A URL that passes a host check, then sends the server somewhere else | CVE-2024-22243 description |
| Client-side redirect to a script URL | A javascript: value assigned to window.location can run script (DOM-based) |
PortSwigger, OWASP WSTG |
RFC 9700 (OAuth 2.0 Security Best Current Practice, January 2025) is direct about this. It says clients and authorization servers must not expose open redirectors, and warns that "Open redirectors can enable exfiltration of authorization codes and access tokens." It also describes an attack that combines a client-side open redirector with the way browsers reattach URL fragments, which can steal implicit-grant tokens even when the redirect URI pattern is narrow. It notes that strict URL matching stops this. See the OAuth vulnerabilities page for the wider flow.
Which parameters and sinks should you check?#
Check any place that takes a destination from the client. These are the usual parameter names: next, url, returnUrl, return_to, redirect, redirect_uri, continue, dest and goto. Check these places in the application too:
- Login, logout, registration and password reset flows that return the user to a page.
- SSO and OAuth callbacks, and the
redirect_uriparameter. - Link trackers, "leaving the site" pages and email click-through links.
- Language and region switchers.
- The
HostandRefererheaders, when the server builds a redirect from them. CVE-2021-22881 below is aHostheader case. - Client-side code that assigns
location,location.href,window.openorlocation.replacefrom a URL value.
Server-side sinks appear as a Location header. Client-side sinks need a browser, because the redirect happens in JavaScript.
Which inputs does a naive check miss?#
A check that looks at the start or the text of the string fails on inputs the browser reads differently. The table shows what three common naive checks accept. These results come from reading the check logic against the WHATWG URL Standard, which says browsers treat a backslash as a slash in special URLs. They are not lab runs, so run each case on your own test application.
| Test input | startsWith("/") |
startsWith("https://app.example.com") |
Contains app.example.com |
Where the browser goes |
|---|---|---|---|---|
//attacker.test/ |
Accepts | Rejects | Rejects | attacker.test |
/\attacker.test/ |
Accepts | Rejects | Rejects | attacker.test (backslash read as slash) |
https://app.example.com@attacker.test/ |
Rejects | Accepts | Accepts | attacker.test (text before @ is userinfo) |
https://app.example.com.attacker.test/ |
Rejects | Accepts | Accepts | attacker.test |
https://attacker.test/?x=app.example.com |
Rejects | Rejects | Accepts | attacker.test |
javascript: URL in a client-side sink |
Rejects | Rejects | Accepts if the text matches | Runs script in the page |
Every row is accepted by at least one common check, so no one string test is enough. The OWASP cheat sheet says: "Do not use raw prefix or suffix matching." It also says to parse the URL with a maintained parser, reject userinfo and ambiguous input, and redirect to the URL you validated, without changing it afterward. Django's url_has_allowed_host_and_scheme follows the same idea and, per its source, checks backslashes, control characters and scheme-relative URLs.
Which real CVEs show open redirects?#
These four NVD entries are open redirects in widely used frameworks. Dates are the NVD publication dates, checked 9 November 2026.
| CVE | Published | Product | Root cause in one line |
|---|---|---|---|
| CVE-2017-7233 | 4 April 2017 | Django 1.8, 1.9, 1.10 before the fixed releases | is_safe_url() treated some numeric URLs as safe |
| CVE-2018-14574 | 3 August 2018 | Django 1.11 before 1.11.15, 2.0 before 2.0.8 | CommonMiddleware had an open redirect |
| CVE-2021-22881 | 11 February 2021 | Rails Action Pack before 6.1.2.1 and 6.0.3.5 | A crafted Host header and an allowed host with a leading dot made the Host Authorization middleware redirect to another site |
| CVE-2024-22243 | 23 February 2024 | Spring Framework | Apps that validated the host of a URL parsed by UriComponentsBuilder could be open to open redirect or SSRF |
Two lessons come from the list. The first two entries are in the redirect check itself, so a home-made check is likely to have the same kind of gap. The Rails and Spring entries show that the redirect target can come from a header or from a parser, as well as from a query parameter. Update the framework and also review your own redirect code.
How do you fix an open redirect?#
Prefer a design that never takes a full URL from the user. OWASP gives this order of choice.
- Avoid redirects that take user input.
- Map a short ID or name to the full URL on the server. OWASP calls this the highest protection, and warns that attackers can enumerate IDs, so do not map anything that must stay secret.
- Accept only a local path, and use a framework helper.
- If external targets are needed, parse the URL and check scheme, host and port against an explicit allowlist.
- For links to other sites, show an interstitial page that names the destination and asks for a click.
The snippets below do option 3 or 4. They use fictional hosts.
Node.js and Express. The helper accepts only a path that starts with one slash, then checks that the parsed URL stays on the same origin:
function safeNext(raw, fallback = "/") {
if (typeof raw !== "string") return fallback;
if (!/^\/(?![\/\\])[^\\\r\n\t]*$/.test(raw)) return fallback;
const base = "https://app.example.com";
return new URL(raw, base).origin === base ? raw : fallback;
}
app.get("/login/done", (req, res) => res.redirect(safeNext(req.query.next)));
Python and Django. This is the pattern from Django's source and release notes. The function name changed in Django 3.0 and the old name was removed in 4.0:
from django.shortcuts import redirect
from django.utils.http import url_has_allowed_host_and_scheme
def after_login(request):
next_url = request.GET.get("next")
if url_has_allowed_host_and_scheme(
url=next_url,
allowed_hosts={request.get_host()},
require_https=request.is_secure(),
):
return redirect(next_url)
return redirect("home")
Python and Flask. Flask has no helper. Normalize the backslash, then allow only a path:
from urllib.parse import urlsplit
from flask import redirect, request
def is_local(target):
t = (target or "").replace("\\", "/")
parts = urlsplit(t)
return t.startswith("/") and not t.startswith("//") and not parts.netloc and not parts.scheme
@app.get("/login/done")
def done():
nxt = request.args.get("next", "")
return redirect(nxt if is_local(nxt) else "/")
Java and Spring. Parse with java.net.URI, allow a local path or an allowlisted host, and reject userinfo. CVE-2024-22243 shows that host checks on a URL parsed by UriComponentsBuilder are not safe, so do not rely on that pattern.
private static final Set<String> ALLOWED_HOSTS = Set.of("app.example.com");
static String safeTarget(String raw) {
try {
URI u = new URI(raw);
boolean local = u.getScheme() == null && u.getRawAuthority() == null
&& raw.startsWith("/") && !raw.startsWith("//") && !raw.contains("\\");
boolean allowed = "https".equals(u.getScheme()) && u.getRawUserInfo() == null
&& u.getHost() != null && ALLOWED_HOSTS.contains(u.getHost());
if (local || allowed) return raw;
} catch (URISyntaxException e) { /* fall through */ }
return "/";
}
// return "redirect:" + safeTarget(next);
C# and ASP.NET Core. Microsoft's documentation shows LocalRedirect, which throws for a non-local URL, and Url.IsLocalUrl:
public IActionResult SomeAction(string redirectUrl)
{
return LocalRedirect(redirectUrl);
}
private IActionResult RedirectToLocal(string returnUrl)
{
if (Url.IsLocalUrl(returnUrl)) return Redirect(returnUrl);
return RedirectToAction(nameof(HomeController.Index), "Home");
}
Ruby on Rails. The Rails API documentation says Rails protects against redirects to external hosts by default, and an app opts out with allow_other_host: true. Search the code for that option. url_from returns the URL only if it is internal, and matches the host exactly:
redirect_to url_from(params[:redirect_url]) || root_url
PHP and Laravel. This snippet is ours and follows the OWASP rule to accept only a local path:
$next = (string) $request->query('next', '/');
$local = str_starts_with($next, '/')
&& !str_starts_with($next, '//')
&& !str_contains($next, '\\');
return redirect($local ? $next : '/');
Redirect to the value you validated and do not change it after the check.
What are the exact rules for OAuth redirect URIs?#
The authorization server must compare redirect URIs by exact string match. RFC 9700 says authorization servers "MUST utilize exact string matching", and the only exception is the port of a localhost redirect for native apps. Patterns are harder to implement correctly. The RFC's example is a wildcard such as https://*.somesite.example/*, which can accept an attacker's domain, and even correct wildcard handling can be exploited through subdomain takeover. The RFC also says http redirect URIs are not allowed, except for native-app loopback redirects.
An exact match does not help if the registered page itself has an open redirect, because the code or token then goes to the open redirector first. Test each registered redirect URI page for open redirects too.
How do penetration testers test and rate an open redirect?#
Testers find the destination parameters, change them to an external test host, and record the redirect. The evidence standard for a finding is the full URL, the redirect response (status and Location header) and a statement of any chain. For a client-side sink, the proof is browser-observed: the page, the URL and the navigation in a real browser.
- Map the flows that return a user to a page, and list every destination parameter.
- Send the test inputs from the table above, one at a time, to a reserved test host. Read the status and
Locationheader, for example withcurl -i. - Check client-side code that sets
locationfrom the URL, and test the same inputs in a browser. - Check
HostandRefererhandling where the server builds a redirect. - Check each registered OAuth redirect URI page for an open redirect.
- If a chain is plausible, show it on a test account and system you are authorized to test. An OAuth code or token reaching a host you control proves the chain.
A standalone open redirect is rated low. The rating rises with a proven chain: a token leak is rated by what the token allows, and an SSRF bypass by what the server can reach. Say in the report which one applies and attach its evidence.
Testing checklist#
- Every parameter that holds a destination is listed, including those in login, logout and SSO flows.
- Each one rejects
//attacker.test/,/\attacker.test/and userinfo URLs such ashttps://app.example.com@attacker.test/. - Host and scheme checks parse the URL. No raw prefix, suffix or substring match.
- Client-side code does not assign user input to
locationwithout a check. -
HostandReferervalues are never used to build a redirect target without an allowlist. - OAuth redirect URIs are matched exactly, and the registered pages have no open redirect.
- Framework and libraries are updated past the CVEs in the table above.
- Rails apps have no unreviewed
allow_other_host: true. - External links go through an interstitial or an ID map.
When should you get a professional test?#
Get one when your product signs users in with OAuth or SSO, sends email links that return users to the app, or runs on several domains. Chains are the hard part: a tester finds the redirect, then checks whether it leaks a code, bypasses a host filter or makes a phishing link. Our web application penetration testing service covers redirect handling in login and OAuth flows, and each finding in the report includes the request, the response and the chain, with a fix for your stack.
Questions
Is an open redirect a vulnerability by itself?
Yes, it is CWE-601, but programs and testers often rate it low when it stands alone. The rating rises when it chains into something with real impact, such as leaking an OAuth code or token, bypassing a host check in an SSRF filter, or giving a phishing link a trusted domain.
What is the difference between an open redirect and SSRF?
An open redirect sends the user's browser to another URL. SSRF makes your server send a request to another URL. They meet when server code follows redirects, or when a host validation flaw allows both. CVE-2024-22243 in Spring Framework is described as open redirect or SSRF.
Which parameters should I test for open redirect?
Look for parameters that hold a destination: next, url, returnUrl, redirect, redirect_uri, continue, dest, return_to and goto. Also check login and logout flows, link trackers, language switchers and SSO callbacks. Read the Location header, and for client-side code read the JavaScript that sets window.location.
Does a startsWith('/') check stop open redirects?
No. A value that starts with // is a protocol-relative URL, and a value that starts with /\ is treated as // by browsers that follow the WHATWG URL rules. Parse the URL, reject authority parts, and compare the result to an allowlist or use your framework's local-redirect helper.
Are open redirects fixed by a redirect confirmation page?
An interstitial page that shows the destination and asks the user to click reduces phishing risk, and OWASP lists it as an option. It does not help OAuth token leaks, because the browser follows the redirect in the authorization flow without a user click. Use exact redirect URI matching there.
Sources
- OWASP Cheat Sheet Series: Unvalidated Redirects and Forwards (checked 9 November 2026)
- OWASP Web Security Testing Guide: Testing for Client-side URL Redirect (checked 9 November 2026)
- CWE-601: URL Redirection to Untrusted Site ('Open Redirect') (checked 9 November 2026)
- PortSwigger Web Security Academy: DOM-based open redirection (checked 9 November 2026)
- RFC 9700: Best Current Practice for OAuth 2.0 Security (January 2025) (checked 9 November 2026)
- Microsoft Learn: Prevent open redirect attacks in ASP.NET Core (checked 9 November 2026)
- Rails API: ActionController::Redirecting (checked 9 November 2026)
- Ruby on Rails Guides: Securing Rails applications (checked 9 November 2026)
- Django documentation: django.utils reference (checked 9 November 2026)
- WHATWG URL Standard (checked 9 November 2026)
- NVD: CVE-2017-7233 (Django is_safe_url) (checked 9 November 2026)
- NVD: CVE-2018-14574 (Django CommonMiddleware) (checked 9 November 2026)
- NVD: CVE-2021-22881 (Rails Host Authorization middleware) (checked 9 November 2026)
- NVD: CVE-2024-22243 (Spring Framework UriComponentsBuilder) (checked 9 November 2026)