Web applications have no shortage of security flaws, but some only reveal themselves once they are actively abused. Open redirects are exactly that kind of flaw. On the surface they look harmless: the user gets a 3xx status code and is sent to another URL. That apparent triviality is precisely what makes them dangerous, because the attacker borrows the trust and the valid TLS certificate of your domain to steer people onto their own site.
That this is a live problem is clear from field data. The SANS Internet Storm Center recorded a marked rise in scanning for open redirect endpoints in February 2026, targeting patterns such as /continue?url=, /redirect?url=, /away?url=, /goto?url= and /jump?url=. Open redirects are not a forgotten relic, they are an actively hunted entry point.
This article explains open redirects from the ground up and then deliberately goes deeper than most overviews. You get a complete, named bypass series that shows why naive checks fail, with the key bypass classes anchored to real, NVD-verified CVEs. After that comes a bypass-resistant validator you can drop straight into Node.js, Python and Java, one that actually defeats every one of those bypasses.
What is an open redirect?
An open redirect (formally CWE-601, "URL Redirection to Untrusted Site") occurs when an application uses user-controlled input as the target of a redirect without checking whether that target is trustworthy. Usually the target URL sits in a query parameter:
<a href="https://example.com/redirect?url=https://evilpage.com">Click here</a>
If the application does not validate the parameter, the user lands on the malicious page. The minimal vulnerable code looks the same in practically every language, which is why the OWASP cheat sheet documents it as a cross-platform pattern, here in PHP:
$redirect_url = $_GET['url'];
header("Location: " . $redirect_url);
An important nuance: not every external redirect is a vulnerability. Search engines, for instance, deliberately forward to arbitrary targets. It becomes a problem where the application offers a redirect that was only meant to be internal and an attacker can freely choose the destination.
The exact place in the OWASP Top 10
Plenty of half-correct claims circulate about the history of the OWASP Top 10. The documented chain is this:
- 2010: Open redirect first appears as its own category, A10:2010 "Unvalidated Redirects and Forwards", in the last slot on the list.
- 2013: The category stays independent as A10:2013. The official CWE taxonomy (CWE-938) lists exactly one member for it: CWE-601. The common claim that open redirect was folded into "Sensitive Data Exposure" back in 2013 is therefore wrong.
- 2017: The category is dropped from the Top 10 because its prevalence had fallen too low.
- 2021: CWE-601 returns, no longer as its own category but as one of 34 CWEs under A01:2021 Broken Access Control, the category that rose from position 5 to position 1.
- 2025: CWE-601 remains under A01:2025 Broken Access Control. Notably, in this edition CWE-918 (Server-Side Request Forgery, SSRF) sits in the same category. Open redirect and SSRF are no longer merely related in theme, they are officially merged in the taxonomy.
For the record, because one well-known learning platform (Snyk Learn) states otherwise: CWE-601 is mapped under A01 Broken Access Control, not under A03 Injection. The authoritative source is the "CWEs Mapped" list on the official OWASP category pages.
Practical attack scenarios
1. Phishing with legitimate domains
The classic. An attacker uses a legitimate-looking URL to steer the victim onto a fake page:
https://trusted-bank.com/login?redirect=https://hacker-site.com/fake-login
The user sees their bank's domain and a valid certificate, suspects nothing, and ends up on a cloned login page. Large, trusted platforms are especially attractive as launch pads: the abuse of a Baidu redirect for phishing is a documented example. In 2024 the pattern surged in combination with quishing, that is QR-code phishing, where a QR code points to an open redirect endpoint on a legitimate domain and forwards from there.
2. Open redirect as part of an XSS chain
If an attacker can control the very start of the redirect string, an open redirect can be escalated into JavaScript injection. Instead of an http URL, a pseudo-protocol is smuggled in:
javascript:alert(document.domain)
This is particularly relevant for DOM-based redirects, where client-side JavaScript writes a value from the URL into a navigation sink. PortSwigger lists vulnerable sinks including location, location.href, location.assign() and location.replace(). A typical vulnerable pattern:
let url = /https?:\/\/.+/.exec(location.hash);
if (url) { location = url[0]; }
How to prevent script execution like this in detail is covered in our article on the basics of XSS.
3. Malware downloads
Open redirects can silently lead users to pages that serve malware automatically or host exploit kits. The user clicks a link pointing at your domain and lands on a drive-by download page.
4. OAuth and the redirect_uri parameter
In the OAuth 2.0 flow, redirect_uri is the destination to which the authorization server sends the user back along with the authorization code. If that parameter is not strictly validated, the authorization server itself becomes an open redirector, and an attacker can capture the authorization code or the token.
The rule recommended by the OAuth working group is unambiguous: exact string matching against the pre-registered redirect URIs. No prefix, no substring and no wildcard comparison, and even a trailing slash (/callback versus /callback/) counts as a mismatch that must be rejected. That this is not theoretical is shown by CVE-2026-5467: in the IAM solution Casdoor (version 2.356.0), the OAuth Authorization Request Handler allowed insufficiently validated redirect_uri manipulation (CWE-601, CVSS 6.1 MEDIUM per NIST). The SANS ISC puts the danger well: an open redirect within the allow-listed URL range may be used to subvert the token.
5. Combination with CSRF and the bridge to SSRF
Combined with Cross-Site Request Forgery, an open redirect can help steer a victim unobtrusively while an unwanted action fires in the background.
The kinship with Server-Side Request Forgery is even tighter: a server that follows a user-supplied URL and trusts a redirect can be walked past a host allow-list via an open redirect. The same parser discrepancy that enables a redirect opens SSRF on the server side. That is exactly why CWE-601 and CWE-918 sit in the same OWASP category in the 2025 edition. Invicti adds the CSP bypass: if an otherwise trusted domain appears in a Content Security Policy and itself has an open redirect, the policy can be undermined.
Why naive checks fail: the bypass series
The most expensive mistake with open redirects is believing that a short string check is enough. It is not. The series below shows the classic bypasses, each with a payload and an explanation of why it works. Assume the trusted domain is trusted.com.
Protocol-relative URLs. A filter that only blocks the keyword http misses the protocol-relative form. The browser supplies the current scheme itself:
//evil.com
////evil.com
Backslash tricks. Chrome and other browsers effectively treat \ in paths like /. A filter that only blocks // is bypassed, and https:/\evil.com is interpreted by the browser as https://evil.com:
/\evil.com
\/\/evil.com
https:/\evil.com
The @ character (userinfo). Under the classic URL syntax (RFC 1738, now superseded by RFC 3986) the URL authority has the form //<user>:<password>@<host>. Everything before the @ is the userinfo component, not the host:
https://[email protected]
This URL looks like a link to trusted.com but navigates to evil.com, because trusted.com is interpreted merely as a username. This very class showed up in a 2024 Tumblr bug bounty case, combined with the backslash trick.
Whitelist substring mistakes. Anyone who only checks whether the target string "contains" the trusted domain is lost:
trusted.com.evil.com
evil.com/trusted.com
In the first case the real host is evil.com (with trusted.com as a subdomain prefix), in the second trusted.com is only a path segment. A real, NVD-verified example is CVE-2026-25477 in AFFiNE (versions up to 0.25.7, fixed in 0.26.0, CWE-601, CVSS 6.1 MEDIUM per NIST). The vulnerable /redirect-proxy endpoint checked the target domain with this regular expression:
new RegExp(`.?${domain}$`).test(target.hostname)
The flaw is in the detail: the dot in .? is an unescaped regex metacharacter that matches any character, and the pattern is not anchored at the start. With t.me on the allow-list, the expression therefore also accepted affineredirect.me, because the string ends on a matching sequence. Attackers could thus redirect through an official-looking AFFiNE link to a deceptively similar domain.
Missing scheme and the startsWith("/") trap. A popular but unsafe shortcut is: "if the URL begins with /, it is local." That is wrong, because //evil.com also begins with / yet is protocol-relative, and /\evil.com begins with / but is read by the browser as //evil.com. A safe relative check must therefore require exactly one leading slash, never // and never /\.
Unicode and IDN tricks. Unicode normalization can shift host boundaries. The full-width dot 。 (%E3%80%82) bypasses a blacklist on the . character, and compatibility ligatures can split a host during normalization:
//google%E3%80%82com
https://evil.c℀.example.com
Double and inconsistent encodings. The most dangerous mechanism is the parser discrepancy: application code, framework and browser interpret the same string differently. This is exactly where CVE-2024-29041 sat in Express.js (fixed in 4.19.2 and 5.0.0-beta.3, classified as CWE-601 and CWE-1286, CVSS 6.1 MEDIUM). Express applied encodeurl internally before writing the URL into the Location header. For malformed URLs, what the browser finally navigated to diverged from what a correctly implemented allow-list had checked earlier. The same class hit Spring Framework in CVE-2024-22243 (affected 5.3.x before 5.3.32, 6.0.x before 6.0.17, 6.1.x before 6.1.4, CVSS 8.1 HIGH per the CNA, VMware). Here UriComponentsBuilder parsed an external URL differently from how it was later actually used, and the NVD explicitly classifies the same bug as both open redirect and SSRF.
The lesson from these three CVEs: it is not enough to use "some" parsing library. The check result and the URL actually used must come from the same normalized representation, otherwise the bypass sits precisely in the gap between them.
The bypass-resistant validator: parse, do not compare
Three principles follow from the bypass series, and every safe validator must implement all of them:
- Parse fully, then compare the host exactly. Never search the raw string, always check the parsed host against an allow-list using exact equality, not a substring or suffix match.
- Define relative targets strictly. The only thing allowed is a path with exactly one leading slash.
//,/\and schemeless hostnames are rejected. - Check and use must not diverge. Backslash variants are additionally checked in their normalized form (the trick Django uses), and only the
httporhttpsscheme is permitted.
Before you write any code, it is worth recalling the canonical OWASP hierarchy: the safest option is to avoid redirects. If a redirect is necessary, the user should only pass an identifier that the server maps server-side to a fixed URL (mapping). Only when genuinely free targets are required does the host allow-list come into play. As a last tier there is a confirmation page that shows the destination.
All the validators below were tested against the complete bypass series from the previous section. Node.js and Python were executed and defeat every payload, and the Java validator was verified by logic review (not at runtime) against the same series, following the same verified logic.
Node.js / Express (WHATWG URL)
The official fix recommendation from the Express advisory is to pre-parse the URL with new URL before any use. That is exactly what this helper does:
const ALLOWED_HOSTS = new Set(['trusted.com', 'www.trusted.com']);
function safeRedirectTarget(input) {
if (typeof input !== 'string' || input === '') return null;
// 1) Relative target: exactly one leading slash, never "//" or "/\"
if (input.startsWith('/') && !input.startsWith('//') && !input.startsWith('/\\')) {
return input;
}
// 2) Absolute URL: let the WHATWG parser decide the real host
let url;
try {
url = new URL(input);
} catch {
return null; // neither a parseable absolute URL nor a clean relative path
}
if (url.protocol !== 'http:' && url.protocol !== 'https:') return null;
if (!ALLOWED_HOSTS.has(url.hostname)) return null;
return url.href;
}
// Usage in Express:
app.get('/redirect', (req, res) => {
const target = safeRedirectTarget(req.query.url);
return res.redirect(target || '/');
});
Why it holds: new URL('https://[email protected]').hostname is evil.com, not trusted.com, and falls out of the allow-list. //evil.com and https:/\evil.com are resolved by the parser to evil.com and rejected. trusted.com.evil.com is not a valid absolute URL string and does not begin with /, so it fails both branches.
Python: Django as reference and a Flask helper
Django solves the problem in django.utils.http.url_has_allowed_host_and_scheme in production-grade form. The decisive trick: the function checks the URL twice, once as-is and once with \ replaced by /, because Chrome treats backslashes like forward slashes.
from django.utils.http import url_has_allowed_host_and_scheme
if url_has_allowed_host_and_scheme(target, allowed_hosts={"trusted.com"}, require_https=True):
return redirect(target)
return redirect("/")
For Flask or any other framework the same principle can be rebuilt compactly. This helper was executed and defeats the entire bypass series:
from urllib.parse import urlsplit
import unicodedata
ALLOWED_HOSTS = {"trusted.com", "www.trusted.com"}
def is_safe_redirect(target, allowed_hosts=ALLOWED_HOSTS):
if not target:
return False
target = target.strip()
# Check the raw AND the backslash-normalised form (Django's trick): Chrome
# treats "\" like "/", so an attacker could smuggle a host past a raw check.
return _check(target, allowed_hosts) and _check(target.replace("\\", "/"), allowed_hosts)
def _check(url, allowed_hosts):
# Three or more leading slashes are absolute to browsers but not to
# urlsplit -> reject outright.
if url.startswith("///"):
return False
# Some browsers ignore leading control characters -> reject.
if unicodedata.category(url[0])[0] == "C":
return False
try:
parts = urlsplit(url)
except ValueError:
return False
# Scheme present but no host (http:///evil.com) -> reject.
if parts.scheme and not parts.netloc:
return False
scheme = parts.scheme or ("http" if parts.netloc else "")
if scheme and scheme not in ("http", "https"):
return False
if not parts.netloc:
# No host: only accept a genuine absolute path ("/dashboard"),
# never a bare "trusted.com.evil.com".
return url.startswith("/")
# Host present: it must match an allow-listed entry EXACTLY. hostname
# strips any "user:info@" prefix, so "[email protected]" is checked
# as evil.com.
return parts.hostname in allowed_hosts
Java / Spring (java.net.URI with an exact host comparison)
The lesson from CVE-2024-22243 matters: the mere existence of a parsing class like UriComponentsBuilder does not protect you if the checked URL and the used URL can diverge. Rely on java.net.URI, parse fully, and compare the host exactly after parsing:
import java.net.URI;
import java.util.Set;
public final class RedirectValidator {
private static final Set<String> ALLOWED_HOSTS =
Set.of("trusted.com", "www.trusted.com");
/** Returns a safe target, or "/" for anything not trusted. */
public static String safeTarget(String input) {
if (input == null || input.isEmpty()) {
return "/";
}
// Browsers treat "\" like "/": check the raw AND the normalised form.
if (isSafe(input) && isSafe(input.replace("\\", "/"))) {
return input;
}
return "/";
}
private static boolean isSafe(String value) {
// Protocol-relative ("//evil.com") and multi-slash forms are unsafe.
if (value.startsWith("//")) {
return false;
}
// A single leading slash is a genuine relative path -> safe.
if (value.startsWith("/")) {
return true;
}
try {
URI uri = new URI(value);
String scheme = uri.getScheme();
if (scheme == null) {
// No scheme and no leading slash -> reject (trusted.com.evil.com).
return false;
}
if (!scheme.equals("http") && !scheme.equals("https")) {
return false;
}
String host = uri.getHost();
// getHost() is null for malformed authorities -> reject on null.
return host != null && ALLOWED_HOSTS.contains(host);
} catch (Exception e) {
return false;
}
}
}
One important caveat about java.net.URI: for certain invalid authorities getHost() returns null. The safe default is therefore to reject on null rather than fall back to the raw authority string.
And the general truth, which the Django source code itself notes in a comment: a true from a host check does not automatically mean "safe". Host validation is necessary, but it is only one building block of a defensive overall design.
Which tools help to find open redirects?
- Burp Suite: Intercepts and manipulates HTTP requests, ideal for running the bypass series manually against an endpoint.
- OWASP ZAP: Open-source scanner that automatically tests for unsafe redirects and documents them.
- Google Dorks: Targeted queries such as
site:example.com inurl:redirect=surface suspicious parameters. - Nikto: Web scanner that detects known vulnerability patterns, including insecure redirects.
- Manual testing with DevTools: Change redirect parameters in the browser and observe the application's reaction.
- Unit tests against the bypass series: The most effective developer-side protection. Turn the payloads from this article into fixed test cases for your validator, exactly as the validators shown here were tested against this very list.
FAQ
What is an open redirect vulnerability?
An open redirect vulnerability (CWE-601) occurs when an application uses user-controlled input as the target of a redirect without checking whether that target is trustworthy. The destination URL usually sits in a query parameter such as ?url=. Because the attacker can freely choose the destination, they can steer users from a legitimate-looking domain to any external site.
How does an open redirect work?
The application takes a user-controlled parameter and passes it straight into a redirect, returning a 3xx status code that sends the browser to that URL. The attacker crafts a link on your trusted domain that points the redirect at their own site, borrowing your domain's reputation and valid TLS certificate. The victim sees a familiar domain, suspects nothing, and lands on the attacker's page.
Is an open redirect a high-severity vulnerability?
As a standalone bug an open redirect is usually rated medium (often CVSS 6.1). Its real danger lies in amplification: it serves as a launch pad for phishing, XSS, CSRF and SSRF. When it chains into a more serious flaw the combined impact rises sharply, which is why Spring's redirect-and-SSRF bug CVE-2024-22243 was scored 8.1 HIGH by the CNA.
How do you prevent an open redirect?
The core mistake is comparing strings instead of parsing URLs. Parse the target URL fully, compare the parsed host exactly against an allow-list (not a substring or suffix match), permit only relative paths with exactly one leading slash, and additionally check backslash variants in normalized form. Safest of all is to avoid free redirects entirely and map a server-side identifier to a fixed URL.
What is the difference between open redirect and SSRF?
An open redirect happens client-side: the browser is sent to an attacker-chosen URL. SSRF happens server-side: the server itself follows an attacker-chosen URL. Both exploit the same parser discrepancy, and since the 2025 edition of the OWASP Top 10 both CWE-601 (open redirect) and CWE-918 (SSRF) sit in the same A01 Broken Access Control category.
Conclusion
Open redirects are an underrated but actively exploited vulnerability, as current scan data and a whole run of fresh CVEs in mainstream frameworks show. The decisive mistake is almost always the same: comparing strings instead of parsing URLs. Anyone who parses the host in full, then compares it exactly against an allow-list, restricts relative targets to a single leading slash, and additionally normalizes backslash variants, closes the entire bypass series. For OAuth, the exact-match rule on redirect_uri applies on top. And because open redirect acts as an amplifier for phishing, XSS, CSRF and SSRF, this diligence pays off twice over. Turn the payloads from this article into test cases before an attacker does it for you.