UnveilTech

UnveilScan Blog

← All articles

Try UnveilScan free

Open redirect bugs: still costing teams in 2026

Posted 2026-04-29 · 6 min read · WEBadvanced

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:

PayloadWhy it bypasses
//attacker.comBrowser interprets as https://attacker.com — protocol-relative URL. Many "starts with /" checks pass.
/\\attacker.comBackslash → forward slash conversion in some parsers. /\ becomes //.
https://example.com.attacker.comexample.com is a subdomain of attacker.com. Substring match passes ("contains example.com").
https://example.com@attacker.comexample.com is the userinfo, attacker.com is the host. Browsers honor the @.
https://attacker.com#@example.comSome 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

  1. 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 compare url.host exactly to the allowlist.
  2. Reject schemes other than http/https. No javascript:, no data:, no file:.
  3. Server-side, not client-side. Validation in the JS handler is bypassable. Validate at the server before issuing the 302.
  4. 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