Open redirect (CWE-601) lets attackers hijack trusted redirect parameters to send users to controlled URLs — phishing, OAuth token theft, and SSRF escalation.
TL;DR
?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:2021 (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 becomes CVSS 9.8 (CVE-2021-29156, ForgeRock OpenAM). An open redirect on a domain in an SSRF allowlist becomes CVSS 8.6 (CVE-2024-2376, LangChain). According to the HackerOne Hacktivity data, open redirects account for approximately 6% of valid reports — one of the most consistently reported classes in bug bounty programs year over year.
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.
The 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 7.6)
Grafana's /public/dashboards/ endpoint treated query string input as a redirect target via path/query parsing confusion. GET /public/dashboards/?attacker.com issued a redirect to attacker.com. Combined with Grafana's session cookie handling, this enabled account takeover — the attacker sent authenticated Grafana users a crafted link, harvested their session via the redirect, and took over their accounts. CVE-2025-6023 followed as a bypass of the initial patch, demonstrating that the string validation fix was insufficient without canonical URL normalization.
CVE-2025-69725 — go-chi RedirectSlashes (CVSS 6.1)
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. Any Go application using chi router with RedirectSlashes enabled was affected. CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N.
CVE-2024-23832 — Mastodon (CVSS 9.4)
Mastodon's remote follow flow accepted unvalidated redirect_uri values. Chained with OAuth token handling, attackers could steal Mastodon account tokens. The CVSS score of 9.4 reflects the account takeover chain, not just the redirect in isolation.
CVE-2024-2376 — LangChain SSRF via Open Redirect (CVSS 8.6)
LangChain's URL document loader followed HTTP redirects without validating the final destination. A URL pointing to a trusted domain that had an open redirect allowed the loader to reach http://169.254.169.254/latest/meta-data/iam/security-credentials/ — yielding AWS IAM credentials from the instance metadata service. This is the canonical open redirect → SSRF → cloud credential theft chain.
CVE-2021-29156 — ForgeRock OpenAM (CVSS 9.8)
The goto parameter in ForgeRock's authentication flow accepted arbitrary external URLs. During OAuth authorization, the authorization code was appended to the goto URL before the redirect — allowing an attacker to exfiltrate authorization codes by setting goto=https://evil.com/capture. This is HackerOne's canonical example for why open redirects in OAuth context receive critical-level treatment.
HackerOne #112614 — Twitter OAuth ($1,470)
Twitter's OAuth callback flow had an open redirect that allowed interception of OAuth access tokens via a crafted redirect_uri. This 2015 report remains the canonical bug bounty reference demonstrating that open redirects in OAuth context warrant high-severity treatment and meaningful bounties. For a more recent example of wildcard redirect_uri exploitation, see HackerOne #2293731 — a $3,000 payout where an expired subdomain allowed an attacker to register a subdomain matching a wildcard redirect_uri entry and capture OAuth authorization codes.
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 fuzzing/redirect-params.yaml
nuclei -u https://target.com -t cves/2025/CVE-2025-4123.yaml
nuclei -u https://target.com -t cves/2025/CVE-2025-69725.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: an open redirect on an OAuth authorization endpoint earns CVSS 8.6-9.8 because it enables authorization code theft. In the SSRF chain variant (server follows the redirect), CVSS reaches 8.6+ due to potential cloud credential exposure, as in CVE-2024-2376 (LangChain).
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. CVE-2021-29156 (ForgeRock OpenAM, CVSS 9.8) is the canonical example.
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. In CVE-2024-2376 (LangChain, CVSS 8.6), the URL loader followed redirects without checking the final destination — trusted.com?redirect=http://169.254.169.254/latest/meta-data/iam/security-credentials/ yielded cloud IAM credentials.
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. Forgejo CVE-2025-30215 and numerous OAuth library CVEs result from implementing prefix matching instead of exact match.
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. Any Go application using chi router with RedirectSlashes enabled and not validating the Host was affected. CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N (6.1).
Grafana's /public/dashboards/ endpoint mishandled query string parsing. A request to GET /public/dashboards/?attacker.com caused Grafana to issue a redirect that incorporated the query string as a URL component. Combined with session cookie handling, this enabled account takeover by sending authenticated users a crafted link. CVE-2025-6023 was a subsequent bypass of the patch for CVE-2025-4123, demonstrating that string-based validation without canonical URL normalization is insufficient.
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 CVE-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 token theft, CVSS elevates to 8.6-9.8 depending on impact (C:H, I:H). CVE-2021-29156 ForgeRock was CVSS 9.8 because it bypassed authentication entirely. CVE-2024-23832 Mastodon was 9.4. 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.