You click "log in" somewhere you trust and land on a pixel-perfect fake that steals your password. The site wasn't hacked. One redirect parameter did it. Why this bug is really a design flaw in the web itself, five disguises it wears, and the small fix plus the UX habit that ends it.
You're logging into a site you trust. You click login, the address bar shows the real domain, then... a perfect copy asking for your password again. You type it. Gone.
The real site did everything right except one tiny thing: after login it bounces you to a page named in the link: site.com/login?next=dashboard. An attacker rewrote it to site.com/login?next=evil.com. The site obeyed, and because the journey started somewhere trustworthy, your guard was down. That's an open redirect, and I want to argue it's not really a bug in that site. It's a bug in the web.
The web never showed you the bounce
Here's the deep cut: browsers show you where you are, never how you got there. A redirect chain (trusted site to attacker site in 300 milliseconds) is invisible. The address bar, humanity's single security indicator, displays the start with full confidence and the landing with none of the history. Every open redirect exploits this exact blindness: borrowed trust, laundered through a legitimate domain, arriving clean.
Browsers can't fix it without breaking half the internet (redirects run logins, checkouts, ads, SSO, everything). So the burden falls on every developer, forever, one parameter at a time. That's why this "low-severity cosmetic bug" has survived decades: it's not a flaw in code, it's a flaw in the medium, patched locally millions of times instead of once globally. I think about that asymmetry every time someone dismisses a bug class as "just" anything.
Five disguises vs naive filters
//evil.com/login: no scheme, but browsers helpfully add one. You're gone.https://trusted.com@evil.com, everything before@is just a username to the browser. The destination is after it.https://trusted.com.evil.comstarts with your domain, ends with theirs. "Starts with" checks wave it through./\evil.com: some browsers quietly convert the backslash. Bye.javascript:...is not a link but a script, executed wherever it's sunk into page code.
Worst case, the "cosmetic" bug chains into account takeover: loose redirect rules on a login system let attackers steal the temporary codes exchanged during sign-in and walk into victims' accounts. The server-side sibling is SSRF: how your own server gets tricked the same way.
Going deeper: the fix (code + habit)
Engineers, short section. Stop pattern-matching strings. Parse properly, check against an exact allowlist:
const ALLOWED = new Set(['https://tirup.in', 'https://blogs.tirup.in']);
export function getSafeRedirectUrl(raw: string | undefined, fallback = '/'): string {
if (!raw || typeof raw !== 'string') return fallback;
if (raw.startsWith('/') && !raw.startsWith('//') && !raw.includes('\')) return raw;
try {
const parsed = new URL(raw);
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') return fallback;
if (ALLOWED.has(parsed.origin)) return parsed.toString();
} catch { return fallback; }
return fallback;
}
// OAuth: exact redirect_uri match (never prefix) + crypto state bound to session.
And the non-code half, for everyone: big sites (Google, banks) show an interstitial ("you’re leaving our site, continue?") on external jumps. It's ugly and it works, because it makes the invisible bounce visible. As a user, cultivate the matching habit: after any login, glance at the address bar. If the domain changed mid-login, stop. The bar is the only witness you’ve got. Interrogate it.
- Open redirect endures because the web hides redirect history: a medium flaw, patched locally forever.
- Developers: exact allowlists, never string prefixes. Users: re-read the address bar after every login.
- One parsing function kills all five disguises. Write it once, use it everywhere.