Are you 21 or older?

Mint

Your Login Redirect Is Probably a Security Hole

· 4 min read

A quick guide to open redirect vulnerabilities — and why your returnUrl parameter needs a babysitter.

A quick guide to open redirect vulnerabilities — and why your returnUrl parameter needs a babysitter.


You ship a login page. Users hit a protected route, get bounced to /login, and after authenticating they land back where they started. Classic UX pattern. Every framework tutorial teaches it.

What they don't teach you is that you just built a phishing accelerator.

The Problem in 30 Seconds

Most login flows pass the "where to go after login" destination as a query parameter:

/login?returnUrl=/account/orders

After successful auth, your code does something like:

const returnUrl = new URL(window.location).searchParams.get('returnUrl');
window.location.href = returnUrl || '/account';

Looks fine. Works great. Ships to prod.

Now an attacker sends your users this link:

/login?returnUrl=https://your-site-login.evil.com

The user sees your domain in the URL bar. They trust it. They log in. Then your own code redirects them to a pixel-perfect clone of your site that harvests their credentials, payment info, or whatever else the attacker wants.

This is an open redirect, and it's been on the OWASP Top 10 list for years. It's trivial to exploit, trivial to fix, and still everywhere.

Why This Catches People Off Guard

A few reasons this slips through:

  1. It doesn't look like a vulnerability. There's no SQL, no script injection, no weird characters. It's just a URL.
  2. It works perfectly in dev. Your tests pass because you're only testing with legitimate paths.
  3. AI-generated code won't flag it. If you're vibe-coding with an LLM and you say "redirect the user back after login," you'll get clean, working, vulnerable code. The AI gives you what you asked for — a redirect. It doesn't add validation unless you ask.
  4. Frameworks don't always protect you. Some do (Rails, Django have opt-in safeguards). Many don't. If you're working with raw query params in any JS framework — Astro, Next, SvelteKit, Express — you're on your own.

The Fix

Validate that the redirect target is a relative path on your own site. That's it. Two checks:

function getSafeRedirectUrl(url) {
  if (typeof url === 'string' && url.startsWith('/') && !url.startsWith('//')) {
    return url;
  }
  return '/account'; // safe default
}

Why both checks?

  • startsWith('/') ensures it's a path, not https://evil.com
  • !startsWith('//') blocks protocol-relative URLs like //evil.com, which browsers resolve to https://evil.com

That's the minimum. If you want to be thorough:

function getSafeRedirectUrl(url) {
  if (typeof url !== 'string') return '/account';

  try {
    const parsed = new URL(url, 'https://placeholder.com');
    if (parsed.host !== 'placeholder.com') return '/account';
  } catch {
    // not a valid URL — fall through to path checks
  }

  if (!url.startsWith('/') || url.startsWith('//')) return '/account';
  return url;
}

Where To Apply It

This is the part people miss. You don't just validate on the client. You validate everywhere the parameter is read:

  • Client-side redirect (after login form submission)
  • OAuth initiation (when you stash the return URL in session/state before redirecting to Google/GitHub/etc.)
  • OAuth callback (when you pull it back out after the provider redirects back to you)
  • Server-side API routes that issue 302 redirects

If there are three places in your code that read returnUrl, you need validation in all three. Not one. Not "the main one." All of them.

A Checklist for Your Codebase

Search your code right now for these patterns:

window.location.href =
window.location.replace(
res.redirect(
Response.redirect(
302
301
returnUrl
redirect_uri
next=
callback=

For every match, ask: Is the destination validated? If it came from a query param, cookie, header, or any user-controlled input — it needs validation.

For the Vibe-Coders

If you're building fast with AI tools, here's what to internalize:

  • LLMs optimize for "works," not "secure." When you prompt for a login flow, you'll get a working login flow. Security constraints are things you have to specify or add yourself.
  • Copy-paste from tutorials has the same problem. Most blog posts and docs show the happy path.
  • Make it a habit: any time you write a redirect that uses external input, pause and validate. It's two lines of code. There is no excuse not to.
  • "But it's just an internal tool" — internal tools get shared. URLs get forwarded. Bookmarks get clicked months later. Validate anyway.

It's Two Lines of Code

Open redirects are embarrassing because the fix is so small. The gap between "vulnerable" and "secure" is a single if statement. The hard part isn't writing the fix — it's knowing you need one.

Now you know. Go grep your codebase.

Your Cart

Your cart is empty

Add items to get started