Open Redirect in Next.js Applications
Next.js provides several redirect mechanisms — redirect() in Server Actions, NextResponse.redirect() in Middleware, router.push() on the client, and the redirects config in next.config.js. When these accept user-controlled URLs without validation, attackers abuse them for phishing by making malicious links appear to originate from your trusted domain.
Scan Your Next.js AppHow Open Redirect Manifests in Next.js
Open redirects in Next.js appear in several patterns: Login flows that redirect users back to their original page after authentication using a ?returnUrl= or ?callbackUrl= query parameter. If the redirect target is not validated, an attacker crafts a link like https://your-app.com/login?returnUrl=https://evil.com that redirects after login. Server Actions using redirect() with user-supplied paths can redirect to external domains. A form action that calls redirect(formData.get('next')) is exploitable if the next parameter contains an absolute URL. Middleware that performs conditional redirects based on request parameters is another vector. A pattern like NextResponse.redirect(new URL(req.nextUrl.searchParams.get('to'))) redirects to any URL the attacker provides. Client-side redirects using router.push() or window.location.href with URL search parameters are also exploitable, though they execute in the browser rather than the server.
Real-World Impact
An attacker sends a phishing email with a link to https://trusted-app.com/login?returnUrl=https://evil-clone.com/login. The victim sees the trusted domain, logs in, and is redirected to a clone of the login page on the attacker's domain. The clone displays "session expired, please log in again," capturing the victim's credentials. Open redirects are also used to bypass URL-based allowlists. If a third-party service allows callbacks only from your domain, an open redirect lets an attacker route through your domain to reach their server, stealing OAuth tokens or other sensitive redirect parameters.
Step-by-Step Fix
Create a safe redirect utility
Build a utility function that validates redirect targets against your domain.
// lib/safe-redirect.ts
export function getSafeRedirectUrl(
input: string | null,
defaultPath = '/'
): string {
if (!input) return defaultPath;
// Only allow relative paths
if (input.startsWith('/') && !input.startsWith('//')) {
// Block protocol-relative URLs and path traversal
const cleaned = input.replace(/\\/g, '/');
try {
const url = new URL(cleaned, 'http://localhost');
// Ensure it stays on same origin
if (url.hostname === 'localhost') {
return url.pathname + url.search;
}
} catch {}
}
return defaultPath;
}
// Usage in Server Action
import { redirect } from 'next/navigation';
import { getSafeRedirectUrl } from '@/lib/safe-redirect';
export async function loginAction(formData: FormData) {
const returnUrl = formData.get('returnUrl') as string;
// ... authenticate user ...
redirect(getSafeRedirectUrl(returnUrl, '/dashboard'));
}Secure Middleware redirects
Validate redirect targets in Next.js Middleware before calling NextResponse.redirect().
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { getSafeRedirectUrl } from './lib/safe-redirect';
export function middleware(request: NextRequest) {
const { pathname, searchParams } = request.nextUrl;
if (pathname === '/redirect') {
const target = searchParams.get('to');
const safePath = getSafeRedirectUrl(target, '/');
return NextResponse.redirect(new URL(safePath, request.url));
}
// For login callback redirects
if (pathname === '/auth/callback') {
const returnUrl = searchParams.get('returnUrl');
const safePath = getSafeRedirectUrl(returnUrl, '/dashboard');
return NextResponse.redirect(new URL(safePath, request.url));
}
}Validate client-side redirects
Apply the same validation for client-side navigation with router.push().
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
import { getSafeRedirectUrl } from '@/lib/safe-redirect';
export default function LoginSuccessRedirect() {
const router = useRouter();
const searchParams = useSearchParams();
const handlePostLogin = () => {
const returnUrl = searchParams.get('returnUrl');
// Never do: router.push(returnUrl)
router.push(getSafeRedirectUrl(returnUrl, '/dashboard'));
};
return <button onClick={handlePostLogin}>Continue</button>;
}Prevention Best Practices
1. Never redirect to user-supplied absolute URLs. Only allow relative paths starting with /. 2. Maintain an allowlist of permitted redirect destinations. 3. Validate returnUrl and callbackUrl parameters in login flows. 4. Strip or reject URLs with different protocols, hostnames, or port numbers. 5. Use URL parsing to verify the hostname matches your domain before redirecting.
How to Test
1. Find all redirect endpoints: search for redirect(), NextResponse.redirect(), and router.push() in your codebase. 2. Test login flows: set returnUrl=https://evil.com and check if the app redirects externally. 3. Try protocol-relative URLs: //evil.com, and backslash variations: /\evil.com. 4. Check Middleware for redirects based on query parameters. 5. Use Vibe App Scanner to automatically detect open redirect vulnerabilities in your Next.js application.
Frequently Asked Questions
Are open redirects in Next.js exploitable for more than phishing?
Yes. Open redirects can bypass OAuth redirect URI allowlists, steal authorization codes by redirecting through your trusted domain, and bypass URL-based security checks in third-party integrations. They are also used in SSRF chains where an internal service follows redirects from a trusted domain.
Does NextAuth.js prevent open redirects in callback URLs?
NextAuth.js validates the callbackUrl parameter against a list of allowed URLs by default. However, if you override the redirect callback or use custom sign-in pages that handle returnUrl manually, you must implement your own validation. Always check custom auth flows for open redirect vulnerabilities.
Can protocol-relative URLs bypass redirect validation?
Yes. A URL like //evil.com is treated as a protocol-relative URL by browsers, redirecting to https://evil.com (or http://evil.com). Simple checks that only look for http:// or https:// prefixes will miss this. Also watch for backslash variations like /\evil.com which some browsers normalize to //evil.com.
Related Security Resources
Is Your Next.js App Vulnerable to Open Redirect?
VAS automatically scans for open redirect vulnerabilities in Next.js applications and provides step-by-step remediation guidance with code examples.
Scans from $5, results in minutes. Get actionable fixes tailored to your Next.js stack.