Open redirect (CWE-601) lets attackers hijack trusted redirect parameters to send users to controlled URLs — phishing, OAuth token theft, and SSRF escalation.
?next=, ?redirect=, ?goto= params; OAuth redirect_uri; JavaScript window.location\\, %09, @endsWith or prefix matching on raw URL stringsOpen redirect is a vulnerability class under CWE-601 (URL Redirection to Untrusted Site) where an application accepts a URL in a request parameter and forwards the user's browser to that destination without validating the target is an authorized host. The redirect mechanism exists for legitimate reasons — sending a user back to the page they were trying to reach after login, or returning them to the referring application after an OAuth flow. The vulnerability arises when the destination URL is taken verbatim from attacker-controlled input.
The server-side form emits an HTTP 302 (or 301, 303, 307) with a Location header containing the attacker's URL. The browser follows it automatically. The client-side (DOM-based) form uses JavaScript: window.location = paramValue, document.location.href = req.query.next. Both fall under OWASP A01:2025 (Broken Access Control) because they allow users to be redirected outside the application's intended control boundary.
Open redirect sits at CVSS 6.1 in isolation — Medium, often rated Low in programs with high thresholds. The true severity multiplier comes from chaining. An open redirect on an OAuth authorization endpoint leaks the authorization code: Spring Security OAuth's CVE-2019-3778 is the textbook case, NVD scoring the code leak 6.5 while the account takeover it enables is what programs pay for (NVD, CVE-2019-3778). An open redirect on a domain in an SSRF allowlist is what Grafana's CVE-2025-4123 exploits — the redirect chained with a client path traversal to a frontend-plugin XSS and, with the Image Renderer plugin installed, a full-read SSRF, rated 7.6 (High) by Grafana (Grafana security advisory, 21 May 2025). CWE-601 is one of the most consistently reported classes in bug bounty programs: HackerOne's CWE discovery index counts 17,199 unique open redirect reports all time — seventh among the 25 weakness classes the index publishes — with 280 new reports in the twelve weeks to 15 October 2026, a 1% decline across the window (HackerOne CWE discovery index, retrieved 15 October 2026). In the CVE database the class is far rarer: the NDSS 2025 study "Do (Not) Follow the White Rabbit: Challenging the Myth of Harmless Open Redirection" (Khodayari, Glauber and Pellegrino) measured open redirects at 1% of 237,470 CVE entries against 37% for XSS, and escalated more than 11.5% of the open redirects it found, on 38% of the affected sites, to XSS, CSRF or information leakage (NDSS Symposium 2025, paper 2025-523).
The baseline pattern is a server-side parameter redirect:
GET /login?next=https://attacker.com HTTP/1.1
Host: trusted-bank.com
HTTP/1.1 302 Found
Location: https://attacker.comThe browser follows the Location header to attacker.com. The user sees trusted-bank.com in their address bar during the initial request — the trust is established before the redirect occurs.
sequenceDiagram
participant U as User
participant B as Browser
participant S as trusted-bank.com
participant A as attacker.com
U->>B: Clicks phishing link to trusted-bank.com/login?next=https://attacker.com
B->>S: GET /login?next=https://attacker.com
Note over B: User sees trusted-bank.com in address bar
S-->>B: 302 Location: https://attacker.com
B->>A: GET / (browser follows redirect)
A-->>B: Phishing login page
Note over U: User believes they are on trusted-bank.comThe redirect chain unfolds in three steps:
req.query.next (or next, redirect, url, goto, return, callback) and passes it directly to res.redirect() without host validation.The DOM-based variant skips the server round-trip:
// Vulnerable Express.js handler
app.get("/post-login", (req, res) => {
const next = req.query.next;
res.redirect(next); // No validation — attacker controls destination
});// Vulnerable client-side redirect
const params = new URLSearchParams(window.location.search);
const next = params.get("next");
window.location = next; // Browser follows to attacker.com| Variant | Technique | CVSS (isolated) | Elevated Chain |
|---|---|---|---|
| URL parameter server-side | ?next=https://evil.com in 30x Location | 6.1 | 8.6 with SSRF |
| Header-based (Host/X-Forwarded-Host) | Host override → redirect URL construction | 6.1 | 8.1 with reset poisoning |
| DOM-based JavaScript | window.location = userParam | 4.3 | 7.4 with cookie theft |
OAuth redirect_uri | Exact match bypass → code theft | 8.6 | 9.8 (full ATO) |
| SSRF chain | Trusted domain redirects to IMDS | 8.6 | 9.1 with cloud creds |
| Path-based | /go/https://evil.com or /r/evil.com | 6.1 | 7.5 |
The most technically interesting aspect of open redirect exploitation is the divergence between URL parsers. A backend validating a URL with Python's urllib.parse will reach different conclusions than the browser using the WHATWG URL standard:
from urllib.parse import urlparse
# Python sees this as a path with no netloc — "safe"
urlparse("\\evil.com").netloc # Returns '' — Python thinks netloc is empty
urlparse("%09https://evil.com").netloc # Returns '' — Python strips tab, sees no scheme
# But WHATWG (Chrome/Firefox) normalizes:
# \\evil.com → //evil.com → https://evil.com (browser follows)
# %09https://evil.com → https://evil.com (browser decodes tab, parses absolute)Additional bypass payloads in active use (Diverto 2024 bypass list):
//attacker.com # Protocol-relative — both slashes required
https:////attacker.com # Multiple slashes — WHATWG strips extras
https:\attacker.com # Backslash — WHATWG normalizes to /
https://trusted.com@evil.com # Userinfo — trusted.com is the username, evil.com is the host
https://trusted.com%40evil.com # URL-encoded @
https://trusted.com.evil.com # Suffix abuse — validate full hostname, not endsWith
https://evil.com#trusted.com # Fragment — trusted.com is the fragment, not the host
%EF%BC%8E # Fullwidth period — Unicode normalization → .url.startsWith("https://trusted.com") is bypassable with https://trusted.com.evil.com and https://trusted.com@evil.com. Never validate redirect destinations by string prefix or suffix. Parse the URL, extract the hostname, and compare against an exact allowlist.
CVE-2025-4123 — Grafana (CVSS 6.1 (NVD))
Grafana's advisory (21 May 2025) describes an XSS built from two flaws: a client path traversal combined with an open redirect. The redirect sends a victim to a website that hosts a frontend plugin, which executes arbitrary JavaScript — no editor permissions required, and with anonymous access enabled the XSS fires without a session. Grafana rates it 7.6 (High, CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:L/A:L) and shipped fixes in 10.4.18, 11.2.9, 11.3.6, 11.4.4, 11.5.4, 11.6.1 and 12.0.0 (+security-01 builds). With the Grafana Image Renderer plugin installed, the same redirect reaches a full-read SSRF; the default Content-Security-Policy (connect-src) blocks the XSS in a default deployment. NVD's own analysis of the same record is 6.1 (Medium) — the isolated-redirect view. CVE-2025-6023 repeated the pattern: an open redirect chained with path traversal to XSS, introduced in Grafana 11.5.0 and fixed in 11.3.8, 11.4.6, 11.5.6, 11.6.3 and 12.0.2 (+security-01 builds) (CNA Grafana, CVE-2025-6023, published 18 July 2025).
CVE-2025-69725 — go-chi RedirectSlashes (CVSS 4.7 Medium)
The RedirectSlashes middleware in go-chi's router normalized path double-slashes: GET //evil.com was processed as a path needing normalization, yielding 302 Location: //evil.com. Browsers resolve protocol-relative URLs (//evil.com) against the current scheme — https://evil.com in production. The flaw was introduced in chi 5.2.2; any Go application with RedirectSlashes enabled and no Host validation was affected. CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:N/A:N (CNA MITRE, CVE-2025-69725, published 19 February 2026).
CVE-2024-23832 — Mastodon (CVSS 9.8 (NVD))
Not an open redirect at all, and it belongs in this list for exactly that reason: Mastodon's flaw was insufficient origin validation, which let an attacker impersonate and take over any remote account (NVD, CVE-2024-23832, published 1 February 2024). GitHub's advisory rates confidentiality impact Low; NVD's primary metric says 9.8 with C:H. Two scorers, two readings of the same record — which is why every score in this article names its source.
CVE-2019-3778 — Spring Security OAuth (CVSS 6.5 Medium)
The canonical open-redirect-to-code-leak case: versions 2.3 before 2.3.5, 2.2 before 2.2.4, 2.1 before 2.1.4 and 2.0 before 2.0.17 accepted a manipulated redirect_uri, causing the authorization server to redirect the resource owner user-agent to an attacker-controlled URI with the leaked authorization code (NVD, CVE-2019-3778, published 7 March 2019). NVD scores the leak 6.5 — the redirect itself is not what makes it dangerous, the code it hands over is.
next, url, redirect, redirect_uri, redirect_url, goto, target, dest, destination, return, returnTo, forward, link, continue, callback, r, u. Use gf redirect (tomnomnom/gf) against Wayback Machine data for historical coverage.https://example.com first (to confirm the redirect mechanism works), then your Burp Collaborator or Interactsh URL.Location header in the response — does it contain your input verbatim? Does the browser follow it to the external domain?redirect_uri and observe whether the authorization code is appended to the crafted URL before the redirect.//attacker.com, \attacker.com, %09https://attacker.com, https://attacker.com@trusted.com, https://trusted.com.attacker.com, https://attacker.com%00trusted.com.Location: /dashboard and Location: https://target.com/path are NOT reported — these are same-origin redirects.OpenRedireX — async fuzzer, sends 50+ bypass payloads per parameter:
echo "https://target.com/login?next=FUZZ" | openredirex -p /path/to/payloads.txtnuclei — template-based scanner with CVE-specific detection:
nuclei -u https://target.com -t http/cves/2025/CVE-2025-4123.yamlDalfox v3.0+ — --open-redirect mode for parameter-level fuzzing:
dalfox url "https://target.com/login?next=1" --open-redirectThe key signal for automated detection is a 30x response where the Location header contains the submitted canary value — confirming user input is reflected into the redirect destination without normalization. DOM-based detection requires JavaScript rendering (headless browser) to observe window.location assignments.
BreachVex detects open redirects via out-of-band canary probes injected into discovered redirect parameters. A confirmed finding requires a 30x Location header containing the canary domain, or a DNS callback confirming server-side fetch. The scan applies eTLD+1 normalization and partner allowlist checks before emitting a finding.
The most robust fix eliminates URL parameters entirely:
# BAD — user-controlled URL passed directly to redirect
from flask import request, redirect
@app.route("/login")
def login():
next_url = request.args.get("next", "/dashboard")
return redirect(next_url) # Open redirect
# GOOD — opaque key maps to server-side destination
import secrets
import redis
@app.route("/login")
def login():
# Store destination before initiating login
state = secrets.token_urlsafe(32)
redis_client.set(f"redirect:{state}", "/original-destination", ex=300)
return redirect(f"/auth/start?state={state}")
@app.route("/auth/callback")
def callback():
state = request.args.get("state", "")
destination = redis_client.get(f"redirect:{state}") or b"/dashboard"
return redirect(destination.decode())# BAD — endsWith is bypassable via trusted.com.evil.com
from urllib.parse import urlparse
def is_safe_redirect(url: str) -> bool:
parsed = urlparse(url)
return parsed.netloc.endswith("trusted.com") # VULNERABLE
# GOOD — exact hostname match after parsing
from urllib.parse import urlparse
ALLOWED_HOSTS = frozenset({"app.example.com", "dashboard.example.com"})
def is_safe_redirect(url: str) -> bool:
if url.startswith("//") or "\\" in url:
return False
parsed = urlparse(url)
# Reject empty scheme with non-empty netloc (protocol-relative after decode)
if not parsed.scheme and parsed.netloc:
return False
if parsed.netloc and parsed.netloc not in ALLOWED_HOSTS:
return False
# Control character check
if any(c in url for c in ["\r", "\n", "\t", "\x00"]):
return False
return True
# Force relative URL as safest fallback
def safe_redirect_url(url: str, fallback: str = "/dashboard") -> str:
return url if is_safe_redirect(url) else fallback// Node.js/Express — GOOD
const ALLOWED_HOSTS = new Set(["app.example.com", "dashboard.example.com"]);
function isSafeRedirect(url) {
try {
// Use WHATWG URL constructor — same parser as browsers
const parsed = new URL(url, "https://app.example.com");
return ALLOWED_HOSTS.has(parsed.hostname);
} catch {
// Relative URLs throw — allow them if they start with /
return url.startsWith("/") && !url.startsWith("//");
}
}
app.get("/redirect", (req, res) => {
const next = req.query.next ?? "/home";
res.redirect(isSafeRedirect(next) ? next : "/home");
});# BAD — prefix matching (vulnerable to path traversal)
def validate_redirect_uri(requested: str, registered: str) -> bool:
return requested.startswith(registered) # VULNERABLE
# GOOD — exact string match per RFC 6749 §3.1.2
def validate_redirect_uri(requested: str, registered: str) -> bool:
return requested == registered # Exact match only — no prefix, no wildcardWildcard redirect_uri registration (*.example.com) is a critical misconfiguration. An attacker registers evil.example.com (if subdomain registration is possible) or exploits any open redirect on any subdomain. RFC 6749 §3.1.2 explicitly prohibits wildcard matching.
An open redirect (CWE-601) occurs when an application accepts a user-controlled URL parameter and forwards the browser to that destination without validating it is an allowed host. The classic pattern is /login?next=https://evil.com — the server issues a 302 Location: https://evil.com and the browser follows it. The trust comes from the original domain; the danger is where it sends the user.
Base CVSS is 6.1 (Medium). In isolation, open redirect is rated Low to Medium. The severity multiplies when chained: on an OAuth authorization endpoint it leaks the authorization code — Spring Security OAuth's CVE-2019-3778 sent the user-agent and the code to an attacker-controlled URI, scored 6.5 at NVD before you count what the code buys (NVD, CVE-2019-3778). In the SSRF chain variant the redirect bypasses the allowlist: Grafana's CVE-2025-4123 reaches a full-read SSRF when the Image Renderer plugin is installed (Grafana security advisory, 21 May 2025).
Server-side open redirect emits a 30x HTTP response with a Location header pointing to the attacker-controlled URL — the browser follows it automatically. Client-side (DOM-based) open redirect uses JavaScript: window.location = userControlledValue or document.location.href = req.params.next — the redirect happens in the browser without a server round-trip. Both are CWE-601; DOM-based is harder for scanners to detect because the redirect logic is in JavaScript, not HTTP headers.
In OAuth 2.0, the authorization server appends the authorization code to the redirect_uri before sending the browser there. If the redirect_uri parameter accepts an open redirect on a trusted domain (e.g., https://trusted.com/redirect?next=https://evil.com), the authorization code is appended to that URL. When the browser follows the redirect to evil.com, the authorization code appears in the URL, the Referer header, and server logs — all attacker-controlled. Spring Security OAuth shipped exactly this in CVE-2019-3778: a crafted authorization request with a manipulated redirect_uri made the authorization server redirect the resource owner user-agent to an attacker-controlled URI with the leaked authorization code (NVD, CVE-2019-3778, published 7 March 2019).
Different URL parsers interpret the same input differently. WHATWG (Chrome, Firefox) normalizes backslashes to forward slashes — //evil.com and \evil.com both resolve to https://evil.com. Python urllib.parse treats \evil.com as a relative path (no host). Node.js legacy url.parse treats //evil.com as protocol-relative. A backend validating with Python may accept \evil.com as safe while the browser resolves it to an external domain. Tab (%09) and newline (%0A) prefixes exploit the same differential: parsers strip control characters differently before host extraction.
SSRF defenses often allow specific trusted domains. If the server fetches a URL from a trusted domain, but that trusted domain has an open redirect, the server follows the redirect to an internal or cloud metadata endpoint. Grafana's CVE-2025-4123 is that chain in production: the open redirect, combined with a client path traversal, sends the victim to a site hosting a frontend plugin that executes arbitrary JavaScript, and with the Image Renderer plugin installed the same redirect reaches a full-read SSRF (Grafana security advisory, 21 May 2025; NVD, CVE-2025-4123).
Primary: next, redirect, redirect_uri, redirect_url, url, goto, target, dest, destination, return, returnTo, forward, link, continue, callback, success, failure. Endpoints: /logout, /login, /oauth/authorize, /sso, /redirect, /go, /out, /external, /r/<id>. Also check the Location header in all 30x responses for user-supplied content reflected verbatim.
Parse the Location header and extract the eTLD+1. If eTLD+1 equals the target's eTLD+1, it is not an open redirect — same-origin redirects are legitimate. Also exclude CDN domains (*.cloudfront.net, *.akamaiedge.net) owned by the target, and declared OAuth partners. A confirmed open redirect has Location: https://<external-eTLD+1-not-on-allowlist> with a 30x status.
RFC 6749 §3.1.2 requires that redirect_uri comparison be an exact string match. No prefix matching, no wildcard matching, no path traversal. The full URI including scheme, host, path, and query string must match the pre-registered URI character-for-character. Any deviation — adding a query parameter, using a subdomain — must be rejected. Spring Security OAuth's CVE-2019-3778 came from exactly that gap: its DefaultRedirectResolver let a manipulated redirect_uri through, and the authorization server redirected the user-agent to the attacker with the code attached (NVD, CVE-2019-3778).
go-chi's RedirectSlashes middleware normalized double-slash paths for trailing slash redirect. A request to GET //evil.com was interpreted as a path needing normalization and the router issued 302 Location: //evil.com. Browsers resolve protocol-relative URLs against the current scheme — so //evil.com becomes https://evil.com. chi 5.2.2 introduced the flaw; any Go application with RedirectSlashes enabled and no Host validation was affected. MITRE, the CNA, scores it 4.7 (Medium) with CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:N/A:N.
Grafana's advisory for CVE-2025-4123 (21 May 2025) describes an XSS built from two flaws: a client path traversal combined with an open redirect. The redirect sends a victim to a website that hosts a frontend plugin, which executes arbitrary JavaScript — no editor permissions needed, and with anonymous access enabled the XSS fires without a session. Grafana rates it 7.6 (High, CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:L/A:L) and shipped fixes in the 10.4.18, 11.2.9, 11.3.6, 11.4.4, 11.5.4, 11.6.1 and 12.0.0 security releases; with the Grafana Image Renderer plugin installed, the same redirect reaches a full-read SSRF. CVE-2025-6023 repeated the pattern — an open redirect chained with path traversal to XSS, introduced in Grafana 11.5.0 and fixed in 11.3.8, 11.4.6, 11.5.6, 11.6.3 and 12.0.2 (CNA Grafana, CVE-2025-6023, published 18 July 2025).
Python: use urllib.parse.urlparse to extract the hostname, then check hostname == 'expected.com' (not endswith). Reject any URL where the parsed scheme is not http/https and the netloc is non-empty but not on the allowlist. Node.js/Express: use the URL constructor (new URL(input)), check url.hostname against an explicit Set of allowed hostnames. Never use string.startsWith or string.includes for host validation — these are bypassed by userinfo abuse (https://allowed.com@evil.com).
Base CWE-601 open redirect is CVSS 6.1 (Medium, CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N). When chained with OAuth authorization code theft, severity rises with what the code buys: NVD scores Spring Security OAuth's CVE-2019-3778 6.5 for leaking the code, while Mastodon's CVE-2024-23832 — insufficient origin validation, not a redirect — is 9.8 (NVD) because it ends in takeover of any remote account. The chain multiplier is the key — always assess the highest-severity chain, not the isolated redirect.
BreachVex uses an out-of-band (OOB) probe strategy: redirect parameters across discovered endpoints receive a unique canary URL. A confirmed open redirect requires a 30x response with Location containing the canary domain, or a DNS/HTTP callback from the server. The scan applies eTLD+1 normalization to eliminate same-origin false positives and checks against the target's CDN/OAuth partner allowlist before reporting.