Open redirect bugs: still costing teams in 2026
Open redirect is the bug class that gets dismissed as "low" by every triage process and then turns out to be the missing link in a $200K bug bounty chain. In 2026 we still see it constantly. The pattern: legitimate-looking URL parameter that takes any arbitrary destination and redirects to it.
The classic shapes
https://example.com/login?redirect=/dashboard
https://example.com/auth?next=/profile
https://example.com/track?url=https://shop.example.com/product/42
The vulnerable variant: pass redirect=https://attacker.com,
next=//attacker.com, or url=javascript:alert(1) and the server
honors it.
Why "just block external URLs" fails
Naive checks miss:
| Payload | Why it bypasses |
|---|---|
//attacker.com | Browser interprets as https://attacker.com — protocol-relative URL. Many "starts with /" checks pass. |
/\\attacker.com | Backslash → forward slash conversion in some parsers. /\ becomes //. |
https://example.com.attacker.com | example.com is a subdomain of attacker.com. Substring match passes ("contains example.com"). |
https://example.com@attacker.com | example.com is the userinfo, attacker.com is the host. Browsers honor the @. |
https://attacker.com#@example.com | Some parsers see fragment, some see different host. |
javascript:alert(1) | If the redirect uses window.location = ... rather than HTTP 302, JS schemes fire. |
data:text/html,<script>... | Same deal with data URIs in some flows. |
Real-world impact: chaining with OAuth
OAuth flows include a redirect_uri. The provider validates that the
redirect_uri matches a registered allowlist for the client. But what if
one of the registered URIs has an open redirect on it?
# Registered redirect URI (legitimate)
https://app.example.com/oauth/callback
# But app.example.com/oauth/callback does:
location.href = req.query.redirect
# Attacker's flow:
1. Phishing link sends victim to OAuth provider with redirect_uri=
https://app.example.com/oauth/callback?redirect=https://attacker.com
2. OAuth provider validates redirect_uri prefix → passes
3. User auths, OAuth returns ?code=ABC to /oauth/callback?redirect=...
4. Server receives code, then redirects to attacker.com?code=ABC
5. Attacker exchanges the code for the user's access token
This was the GitHub OAuth bug bounty pattern. Multiple providers paid $5K-$25K for variants of this chain throughout 2020-2024. It's still being found in 2026 because OAuth allowlists are often a single registered URI, and any open redirect on that URI breaks the model.
Phishing pretext stamping
Even without OAuth, an open redirect on your domain stamps a phishing email with your brand. The attacker emails a victim:
From: notifications@bigbank.com
Subject: Urgent: account verification
Click here to verify:
https://bigbank.com/track?url=https://bigbank-secure-login.attacker.com/
The URL is unambiguously on bigbank.com. The user hovers, sees bigbank.com, clicks. The redirect lands them on the attacker's phishing page. Email security gateways that scan URL reputation see bigbank.com — clean. Users who train themselves to "look at the URL" — fooled.
The right validation pattern
- Allowlist by full URL or host, never by substring. If you allow only relative paths, check that the input starts with
/AND the second character is not/AND not\. If you allow a list of hosts, parse the URL with a real parser and compareurl.hostexactly to the allowlist. - Reject schemes other than http/https. No
javascript:, nodata:, nofile:. - Server-side, not client-side. Validation in the JS handler is bypassable. Validate at the server before issuing the 302.
- Append a signed token. Sign the redirect target with a server-side HMAC and include the signature in the URL. Reject any redirect missing or with a wrong signature. Used by AWS Console signin URLs.
Go example (parser-based)
func validateRedirect(target string) (string, error) {
u, err := url.Parse(target)
if err != nil { return "", err }
if u.Scheme != "" && u.Scheme != "https" { return "", errors.New("bad scheme") }
if u.Host != "" && u.Host != "app.example.com" { return "", errors.New("bad host") }
// For relative redirects: u.Host == "" and u.Path != ""
if !strings.HasPrefix(u.Path, "/") { return "", errors.New("must be absolute path") }
if strings.HasPrefix(u.Path, "//") { return "", errors.New("protocol-relative") }
if strings.HasPrefix(u.Path, "/\\") { return "", errors.New("backslash escape") }
return target, nil
}
Why we don't actively probe for it
Active probing for open redirect requires sending tens of variants per parameter on tens of parameters. That's a fuzzing pattern, which violates our passive-by-default rule. The right tool for this is your own QA suite (with the URL parameter inventory from your route table) or a pentest engagement.
What we do flag: response headers that hint at unsafe redirect behavior — specifically
Location: //... patterns observed during HTTP probes and any
X-Frame-Options / Content-Security-Policy: frame-ancestors
combinations that don't restrict iframe embedding (relevant for clickjacking-extended
redirects).
Audit your redirect endpoints
Free Basic scan flags missing CSP and lax frame policies. Pair with internal redirect testing.
Run a scan