Skip to content

Open Redirect Prevention

Open redirects are one of the most common vulnerabilities in authentication flows. They are easy to introduce and easy to prevent. This guide explains the threat and provides concrete implementations for every supported framework.


An open redirect occurs when your application redirects users to a URL taken from user input without validating it. In auth flows, the most common pattern is a “redirect after login” parameter:

https://app.example.com/login?redirect=/dashboard

After the user logs in, your application redirects them to /dashboard. This is a good user experience — the user lands where they intended to go.

The vulnerability appears when an attacker modifies the redirect parameter:

https://app.example.com/login?redirect=https://evil.example.com/phishing

If your application blindly redirects to this URL after login, the user lands on an attacker-controlled page. Because the login happened on your legitimate domain, the user trusts the redirect. The phishing page can then:

  • Display a fake “session expired” message and collect credentials again
  • Mimic your application’s UI to steal sensitive data
  • Serve malware

Open redirects are particularly dangerous in authentication flows because:

  1. Trust is at its highest. The user just entered their password on your real login page. They trust the next page they see.
  2. The URL looks legitimate. The initial link points to your domain, which passes URL inspection by security-conscious users.
  3. It bypasses phishing defenses. Email filters and browser warnings check the link domain, which is yours. The redirect happens after the click.

The rule is simple: never redirect to user-supplied absolute URLs. Only allow relative paths.

lib/safe-redirect.ts
/**
* Validates a redirect URL to prevent open redirect attacks.
* Only allows relative paths (starting with "/", not "//").
* Returns the fallback URL if the input is invalid.
*/
export function safeRedirect(url: string | null | undefined, fallback = '/'): string {
if (
!url ||
!url.startsWith('/') ||
url.startsWith('//') ||
url.startsWith('/\\') ||
url.includes('://')
) {
return fallback;
}
return url;
}

This function rejects:

InputReasonResult
null / undefined / ""MissingFallback
https://evil.comAbsolute URL with protocolFallback
//evil.comProtocol-relative URL (browser follows it)Fallback
/\evil.comBackslash trick (some browsers normalize \ to /)Fallback
javascript:alert(1)Does not start with /Fallback
/dashboardValid relative path/dashboard
/settings/profileValid relative path/settings/profile

Step 1: Pass the redirect parameter to the login page

Section titled “Step 1: Pass the redirect parameter to the login page”

When redirecting an unauthenticated user to the login page, include their intended destination:

// In middleware or a route guard
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('redirect', request.nextUrl.pathname);
return NextResponse.redirect(loginUrl);

Step 2: Include the redirect in the login form

Section titled “Step 2: Include the redirect in the login form”
Login form
export default function LoginPage({
searchParams,
}: {
searchParams: { redirect?: string };
}) {
return (
<form action={loginAction}>
<input
type="hidden"
name="redirect"
value={searchParams.redirect ?? '/dashboard'}
/>
{/* email, password fields */}
<button type="submit">Sign in</button>
</form>
);
}
Login action
import { safeRedirect } from '@/lib/safe-redirect';
export async function loginAction(_prev: unknown, formData: FormData) {
const email = formData.get('email') as string;
const password = formData.get('password') as string;
const redirectTo = formData.get('redirect') as string;
const session = auth();
const success = await session.attempt({ email, password });
if (!success) {
return { error: 'Invalid email or password.' };
}
// Validate the redirect URL — never trust user input
redirect(safeRedirect(redirectTo, '/dashboard'));
}

app/actions/login.ts
'use server';
import { redirect } from 'next/navigation';
import { auth } from '@/lib/auth';
import { safeRedirect } from '@/lib/safe-redirect';
export async function loginAction(_prev: unknown, formData: FormData) {
const email = formData.get('email') as string;
const password = formData.get('password') as string;
const redirectTo = formData.get('redirect') as string;
if (!email || !password) {
return { error: 'Email and password are required.' };
}
const session = auth();
const success = await session.attempt({ email, password });
if (!success) {
return { error: 'Invalid email or password.' };
}
redirect(safeRedirect(redirectTo, '/dashboard'));
}
middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const hasSession = request.cookies.has('ideal_session');
if (!hasSession) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('redirect', request.nextUrl.pathname);
return NextResponse.redirect(loginUrl);
}
return NextResponse.next();
}

For applications that need tighter control, maintain an explicit list of allowed redirect paths:

lib/safe-redirect.ts
const ALLOWED_REDIRECT_PREFIXES = [
'/dashboard',
'/settings',
'/profile',
'/projects',
];
export function safeRedirect(url: string | null | undefined, fallback = '/'): string {
if (
!url ||
!url.startsWith('/') ||
url.startsWith('//') ||
url.startsWith('/\\') ||
url.includes('://')
) {
return fallback;
}
// Check against allowlist
const isAllowed = ALLOWED_REDIRECT_PREFIXES.some(
(prefix) => url === prefix || url.startsWith(prefix + '/'),
);
return isAllowed ? url : fallback;
}

This approach prevents redirects to paths like /logout (which could log the user out immediately after login) or /api/delete-account.


Prefer encoding the redirect target in the URL path or as a form field rather than a query parameter when possible:

# Less ideal — redirect in query string
/login?redirect=/dashboard
# Better — redirect as a hidden form field
<input type="hidden" name="redirect" value="/dashboard" />

Query strings are logged by web servers, proxies, and analytics tools. If the redirect contains any sensitive path (e.g., /settings/api-keys), it becomes visible in logs. Form fields submitted via POST are not logged in URLs.


  1. Never redirect to absolute URLs from user input
  2. Always validate redirect parameters with safeRedirect() or an equivalent
  3. Check for // — protocol-relative URLs bypass the “starts with /” check
  4. Use a fallback — if the redirect is invalid, send users to a safe default like /dashboard
  5. Consider an allowlist for applications with strict security requirements