Open redirect on the login page: how to fix it
An open redirect on a login page means that your app sends users to any URL that a link names, so accept only paths on your own site and check the parsed origin before each redirect. An attacker puts a link with your real domain in an email. The user logs in on your real page. Then your app sends the user to the fake page of the attacker.
This guide shows how to find the problem, how to fix the code, and how to fix the allow list of your auth provider. It also shows what to check in your logs.
What does “open redirect on login page” mean?
It means that the login page reads a destination from the URL and sends the user there after login. The parameter has names such as next, redirect, returnTo, returnUrl, callbackUrl, and redirectTo. If the app does not check the value, the destination can be any website.
The OWASP cheat sheet on unvalidated redirects describes the attack. An attacker changes the URL input to a malicious site. Then the attacker uses the link for a phishing scam. The server name in the link is the same as the real site, so the link looks trustworthy. The weakness has the identifier CWE-601.
This is a typical attack on a login page:
- The attacker creates the link
https://your-app.example/login?next=https://fake.example/login. - The attacker sends the link to your users. The domain in the link is yours.
- The user enters the real email and the real password on your real page.
- Your app sends the user to
fake.example. That page looks like your login page and says “Session expired, log in again”. - The user enters the credentials a second time. The attacker keeps them.
The user has no reason to doubt the link. The first page was real, and the browser showed your domain. For this reason, the login page is the most valuable place for an open redirect.
AI coding tools often write this code. You ask for “redirect the user back to the page that they wanted”. The tool reads next from the query string and calls the redirect function. The tool does not add a check, because the feature works.
How do I check my login page for an open redirect?
Test the login flow with external values in each redirect parameter. Use a domain that you own, or use example.org. Do not use the domain of another company.
First find the parameter name. Log out, open a protected page, and read the URL of the login page that appears. Look for next, redirect, returnTo, or a similar name. Also search the code:
grep -rn -E "searchParams.get\(.(next|redirect|returnTo|returnUrl|callbackUrl)|req\.query\.(next|redirect|returnTo)|params\[:(next|redirect|return_to)\]" src app pages lib 2>/dev/null
Then test each value in the table. Add the value to the parameter, log in with a test account, and look at the address bar after login.
| Test value | What it tries | Expected result |
|---|---|---|
https://example.org |
A full external URL | Stays on your site |
//example.org |
A URL without a scheme | Stays on your site |
/\example.org |
A backslash after the slash | Stays on your site |
https:example.org |
A scheme without slashes | Stays on your site |
javascript:alert(1) |
A script scheme | Stays on your site |
https://your-app.example.evil.example |
A host that starts with your domain | Stays on your site |
/settings |
A normal path | Goes to /settings |
Encode the special characters if your browser changes them. For example, write // as %2F%2F in some tests. Also test the logout page, the signup page, and the email confirmation page. These pages often use the same redirect code.
You can test the first response of a login API with curl. Replace the URL and the body with the values of your app:
curl -s -i -X POST "https://your-app.example/api/login?next=https://example.org" \
-H "Content-Type: application/json" \
-d '{"email":"test@your-app.example","password":"test-password"}' | grep -i "^location"
If the Location header shows https://example.org, the server follows the external URL. If your app redirects in the browser with JavaScript, the test must use a browser. Look for window.location and router.push in the code that reads the parameter.
Warning: Test only your own app and only with a test account.
How do I fix the open redirect? Fix it now, in order of urgency
Do the steps in this order. Steps 1 and 2 close the hole. The other steps close related holes.
- Find every redirect that reads user input. Use the
grepcommand above. Check the login, logout, signup, password reset, and email confirmation code. Make a list of the places. - Replace each raw redirect with a check function. Use the function in the next section. The function accepts only a path on your own site.
- Remove the parameter if you do not need it. If every user goes to the dashboard after login, delete the code that reads
next. This is the safest fix. - Fix the redirect allow list of your auth provider. Remove wildcard entries on production. See the section about Supabase below.
- Deploy and test again. Use the table from the previous section. Each external value must lead to the safe default page.
- Add the test cases to your test suite. A later change by an AI tool can remove the check without notice.
Caution: Do not fix the problem with a check such as url.includes("your-app.example") or url.startsWith("https://your-app.example"). Attackers bypass these checks with hosts such as your-app.example.evil.example. The OWASP cheat sheet says: do not use raw prefix or suffix matching. Parse the URL and compare its parts.
The check function
The safest rule is simple. Accept only a value that starts with one / and that stays on your own origin after parsing. The function below does this in TypeScript. It works in Next.js, Express, and other Node code.
export function safeNext(raw: string | null, origin: string): string {
const fallback = "/dashboard";
if (!raw) return fallback;
if (!raw.startsWith("/") || raw.startsWith("//") || raw.includes("\\")) return fallback;
const url = new URL(raw, origin);
if (url.origin !== origin) return fallback;
return url.pathname + url.search + url.hash;
}
The function has four quick exits. It rejects a missing value, a value that does not start with /, a value that starts with //, and a value that has a backslash. Then it parses the value against your own origin. If the parsed origin differs from your origin, the function rejects the value. The function returns the parsed path, not the raw text.
The parse step is important. Browsers remove some characters, such as tabs and line breaks, from a URL before they read it. A value such as / followed by a tab and /example.org passes a simple startsWith test, but the parser reads it as an external host. The origin comparison catches this case. We tested the function with the values in the table above, and it returned the fallback for each external value.
Use the function at the point where you redirect:
// Next.js route handler, after a successful login
import { NextResponse } from "next/server";
export async function POST(req: Request) {
// ... check the password and create the session ...
const origin = new URL(req.url).origin;
const next = new URL(req.url).searchParams.get("next");
return NextResponse.redirect(new URL(safeNext(next, origin), origin), 303);
}
On a production host, do not take the origin from a request header that the client controls. Use a fixed value from your environment, for example process.env.SITE_URL. A fixed origin does not change when an attacker sends a different Host header.
Alternative: map a short name to a path
The OWASP cheat sheet gives a stronger option. Let the user send a short name, and map the name to a full path on the server. For example, next=billing maps to /settings/billing.
const targets: Record<string, string> = {
billing: "/settings/billing",
projects: "/projects",
};
const dest = targets[next ?? ""] ?? "/dashboard";
An attacker cannot send a URL, because the code accepts only the names in the list. Keep the list short. Use names, not numbers, so that nobody can count through all targets.
Fix the redirect list of your auth provider
Social login, magic links, and password reset also use a redirect URL. Your auth provider checks that URL against an allow list. If the list is too wide, the provider sends the user, and sometimes the token, to the wrong place.
For Supabase, the redirect URLs guide says that the redirectTo value should match the list on the URL Configuration page. The guide also says that the Site URL is the default redirect when you do not set redirectTo. Change the Site URL from http://localhost:3000 to your production URL.
The same guide explains that the wildcard ** matches any sequence of characters. It recommends that you set the exact redirect URL path for your site URL in production. Use this method:
- Open the URL Configuration page of your Supabase project.
- Set the Site URL to your production URL.
- Remove the entries with
**that you added for tests. - Add the exact production callback URL, for example
https://your-app.example/auth/callback. - Keep
http://localhost:3000/**only in a separate development project.
For more about the flows of magic links and password reset, read auth flows. The SSRF and open redirect pattern page shows the same weakness in OAuth callbacks.
How do I check the damage?
An open redirect leaves few traces. You cannot prove that nobody used it, but you can look for signs.
- Search the access logs for redirect parameters with external values. Look for requests to the login page where
next,redirect, orreturnTocontainshttp,//,%2F%2F, or a backslash. Use a command such as the one below. - Look at the sources of these requests. Real users do not write external URLs into your login link. A set of such requests from many different users shows a phishing campaign.
- Read the referrer data. If you use analytics, look at traffic that comes from email clients or chat tools to the login page with an unusual parameter.
- Check your support inbox. Users sometimes report a “strange login page” or a message that asks them to log in again.
- Check for logins from new places. If an attacker gained credentials, the attacker logs in from a new address. Look for logins from unusual countries and for many failed logins.
- Check what the redirect carries. If your app puts a session token, a code, or a reset token in the redirect URL, the external site can read it. Treat each of these tokens as leaked and invalidate it.
grep -E "(next|redirect|returnTo|returnUrl|callbackUrl)=(https?(:|%3A)|//|%2F%2F|%5C)" access.log | head -50
Replace access.log with the log file of your host. On a hosted platform, use the search function of the log page in the dashboard. Many platforms keep logs for a short time only, so download the logs now.
If you find signs of a phishing campaign, tell your users. Ask them to change their passwords and to turn on two-step login if you offer it. Do not wait until you are sure.
How do I stop it from happening again?
Use these controls to keep the hole closed.
- Use one function for each redirect. Put
safeNextin one file and import it everywhere. Do not write a new check at each place. - Test the bad values in your suite. Run each value from the table through the function. A test for a function is fast and needs no browser. Also test one full login in a browser test.
- Do not copy the URL from the request into the response. The same rule applies to the
Refererheader and to theHostheader. - Use exact URLs in every allow list. This rule applies to your auth provider, to OAuth apps, and to payment return URLs.
- Add a rule for your coding tool. Write in the project rules: “Never redirect to a URL from user input. Use
safeNext.” Search each pull request forredirect(andwindow.locationwith a variable. - Consider a confirmation page for external links. The OWASP cheat sheet suggests a page that tells users they leave your site and shows the destination. Use it if your product must send users to external sites.
- Scan the deployed app after each release. A scan sends the bad values to the live login flow and shows the real behavior.
For the full list of checks before launch, read the production security checklist.
When should I get help?
Get help in these cases:
- Your logs show many requests with external redirect values, and you think that users entered their credentials on a fake page.
- Your app puts tokens or codes in the redirect URL, and you cannot tell which ones were exposed.
- You cannot find all the places that redirect, for example because an AI tool generated the code in many files.
- You use a custom OAuth server or a custom magic link flow, and you do not know how the redirect URL is checked.
- Your app stores personal data and you saw signs of account takeover. You may have a duty to report a breach.
After you finish the fix, test the login page again with each value from the table. The browser must stay on your site each time.
Common questions
Is an open redirect a serious vulnerability?
Can I block redirects by checking that the URL contains my domain?
Do I need a redirect parameter at all?
Does the same problem exist in OAuth and magic links?
How do I test my fix?
Test your login redirect
A VibeEval scan sends external URLs in the redirect parameters of your login flow and reports each one that your app follows.
14-day free trial · No credit card · Cancel anytime