What Is an Open Redirect Vulnerability? (And How to Fix It)
An open redirect lets anyone use your domain to send people to any other site — perfect for phishing links that look trustworthy. How it happens with ?next= and ?returnTo= parameters, why it's worse with OAuth, and the safe ways to redirect after login.
Many apps redirect users after an action — most often after login:
https://yourapp.com/login?next=/dashboard
After signing in, the app sends you to /dashboard. Handy. But if the app redirects to whatever is in next, then this also works:
https://yourapp.com/login?next=https://evil-site.example/fake-login
That's an open redirect: your site will forward visitors to any URL an attacker chooses.
Why it matters
On its own, a redirect doesn't steal anything. The danger is trust:
- Phishing. The link starts with your real domain, so it looks safe — in an email, a chat, a search result. After logging in on your real site, the user lands on the attacker's page that says "Session expired, please log in again" and collects their password.
- Bypassing link filters that allow your domain.
- Stealing OAuth tokens. If an OAuth flow's redirect can be bounced through your open redirect, access codes or tokens can end up on the attacker's site. (Sign in with Google explained)
- Chaining with other bugs, such as server-side request forgery. (SSRF explained)
The vulnerable pattern
app.post('/login', async (req, res) => {
// ...check password
res.redirect(req.query.next) // ❌ redirects anywhere
})
Common parameter names: next, redirect, returnTo, return_url, continue, url, dest.
How to fix it
1. Only allow relative paths on your own site
function safeRedirect(target) {
if (typeof target !== 'string') return '/'
// must start with a single slash; reject //evil.com and /\evil.com
if (!target.startsWith('/') || target.startsWith('//') || target.startsWith('/\\')) {
return '/'
}
return target
}
res.redirect(safeRedirect(req.query.next))
The // check matters: //evil.com is a "protocol-relative" URL that browsers treat as https://evil.com.
2. Or parse and compare the origin
function safeRedirect(target) {
try {
const url = new URL(target, 'https://yourapp.com')
return url.origin === 'https://yourapp.com' ? url.pathname + url.search : '/'
} catch {
return '/'
}
}
3. Or use an allow-list
If you must redirect to other domains (your marketing site, a docs site), keep a fixed list and redirect only to those. Better still, pass a key instead of a URL — ?next=billing mapped to a known path in your code.
4. Don't use string checks like "contains"
if (target.includes('yourapp.com')) is easily fooled: https://yourapp.com.evil.example, https://evil.example/?yourapp.com.
Frameworks and auth libraries
Most auth libraries (NextAuth/Auth.js, Better Auth, Clerk, Supabase) validate callback URLs against your configured domains — if configured correctly. Set allowed redirect URLs precisely; avoid wildcards. In Supabase, check the Redirect URLs list in Auth settings. (Clerk vs Auth0 vs Supabase Auth)
AI-generated login code often includes a next/redirect parameter without validation. It's worth searching your codebase for redirect( and checking each one. (Security checklist for vibe-coded apps)
Testing for it
Try your redirect parameters with:
https://example.com//example.com/\example.comhttps:example.comhttps://yourapp.com.example.com
None should take you off your site.
EasySpawn gives Claude Code a full copy of your app on its own server — ask it to find every redirect and validate it, then test the result live. See how it works or join the waitlist.
Related: The OWASP Top 10 Explained · "Sign in with Google" Explained · Add Login to an AI-Built App · SSRF Explained
Keep reading
"Your Connection Is Not Private" on Your Own Site: Causes and Fixes
When visitors see NET::ERR_CERT_DATE_INVALID, ERR_CERT_COMMON_NAME_INVALID or ERR_CERT_AUTHORITY_INVALID on your site, the SSL certificate is expired, for the wrong name, or incomplete. How to tell which, and how to fix each one.
What Is HSTS? Strict-Transport-Security Explained
HSTS tells browsers to only ever use HTTPS for your site, closing the gap where a first HTTP request could be intercepted. How the header works, max-age, includeSubDomains and preload, how to roll it out safely, and how to set it in Nginx, Caddy, Express and Next.js.