Example finding How it works Coverage PENTEST METHODOLOGY DOCS PRICING FAQ MCP CONTACT LOG IN SIGN UP →

Admin page accessible without login: how to fix it

FIX / OPEN ADMIN PAGE / 9 MIN READ

If an admin page opens without a login, the server does not check who the user is, so add a server-side check for the login and the admin role on the page, on every API route behind it, and on the data. A redirect in the browser is not a check. The server must refuse the request before it sends any admin page or data.

This guide shows how to confirm the problem, how to fix it in order, and how to check if someone already used the page.

What does “admin page accessible without login” mean?

It means that a request without a session cookie or token still gets an admin page, admin data, or an admin action. The attacker does not need to guess a password. The attacker only needs to know the address, or to find it.

OWASP lists this weakness in its Broken Access Control item. The item names “force browsing to authenticated pages as an unauthenticated user or to privileged pages as a standard user.” The prevention advice starts with: “Except for public resources, deny by default.”

AI coding tools often create this problem. You ask for an admin dashboard. The tool builds the screen and adds a check in the browser. The check shows a login form when the user is missing. The app looks protected when you test it as a normal user. The server still sends everything.

These are the usual causes:

  • A client-side guard only. A React component or a router hook redirects to /login, but the server sends the page and the data first.
  • A hidden link. The app hides the admin menu for normal users, but the route and the API stay open.
  • A middleware gap. The route check covers /admin, but not /admin/users, /api/admin/users, or a server action.
  • A flag in the browser. The app reads isAdmin from localStorage or from a cookie that the user can edit.
  • Open data. The page shows a login form, but the database table behind it has no access rules.
  • A route that someone forgot. An older admin page, a test page, or a /debug route stays in the code.

The Next.js authentication guide says that a layout check is not enough. It states that a layout “does not control whether the rest of the route renders.” It also says that returning null in a layout when the user is not authorized “is not recommended,” because the app has several entry points. Nested routes and Server Actions stay reachable.

How do I confirm that my admin page is open?

Test as a stranger. Use a browser window with no login, and use curl.

  1. Open a private browser window. Do not sign in.
  2. Open the address of the admin page, for example https://your-app.example/admin.
  3. Check what you see. A real admin screen, a table of users, or a flash of admin content before a redirect means a failure.
  4. Check the result with curl. This shows what the server sends, not what the browser shows after the scripts run.
curl -s -o /dev/null -w "%{http_code} %{redirect_url}\n" https://your-app.example/admin
curl -s https://your-app.example/admin | head -50

A correct result is a 401 or 403 status code, or a 302 redirect to the login page, with no admin content in the body. A 200 status code with admin content is a failure.

  1. Find the API calls that the admin page makes. Sign in as an admin in a normal window. Open DevTools and select the Network tab. Use each admin function once. Note each request address and each method.
  2. Repeat each request without the session. Use curl and remove the cookie and the Authorization header.
curl -i https://your-app.example/api/admin/users
curl -i -X POST https://your-app.example/api/admin/delete-user \
  -H "Content-Type: application/json" -d '{"id":"123"}'

Warning: Run the write requests only on a test account or a test project. A request that works also changes real data.

  1. Check the common names. Request these paths without a login: /admin, /dashboard, /admin/users, /api/admin, /api/users, /api/export, /manage, /internal, /debug, and /_next/data/. Add the paths that your own app uses.
  2. If the app uses a hosted database, test it directly. For Supabase, call the REST endpoint of the admin table with only the anon key. The anon key guide shows how. A list of rows means that the data is open, even if the page is protected.

Any admin content in a response to a request without login is a failure. Start the fix now.

How do I fix it now?

Do these steps in order. Step 1 stops the loss at once. The other steps build the correct check.

Caution: If you take the admin page offline, you cannot use it until the fix is ready. This is the correct trade. An open admin page costs more than a short wait.

  1. Close the page. Take the route offline, or block it at the host. Many hosts let you set a password or a deny rule for a path. This is a short-term step only.
  2. Rotate secrets that the page could show. If the admin page shows API keys, tokens, or links, rotate them. Treat anything that the page showed as seen by others.
  3. Add a server-side check for the login. The server must read the session from the cookie or the token, and verify it. If the session is missing or not valid, return 401 or redirect to the login page.
  4. Add a check for the role. A user who is signed in is not always an admin. Check the role on the server, and return 403 if the user is not an admin.
  5. Put the same check on every API route and server action behind the page. Treat each one as a public endpoint. The Next.js guide gives the same advice for Server Actions and Route Handlers.
  6. Protect the data. Add rules at the database, so the data stays closed even when a route check is missing.
  7. Deny by default. Make the check apply to the whole /admin area and the whole /api/admin area, so that a new route is closed from the first day.
  8. Test again from outside. Repeat the curl tests from the previous section.

What does a correct check look like in Next.js?

The Next.js guide recommends a Data Access Layer. This is one server-only function that checks the session. Every page, route handler, and action calls it. The example below follows the pattern of the guide. It uses Supabase for the user. The function createServerSupabase is a placeholder for the server client of your project:

// app/lib/dal.ts
import "server-only";
import { cache } from "react";
import { redirect } from "next/navigation";
import { createServerSupabase } from "@/app/lib/supabase-server";

export const requireAdmin = cache(async () => {
  const supabase = await createServerSupabase();
  const { data: { user } } = await supabase.auth.getUser();

  if (!user) redirect("/login");
  if (user.app_metadata?.role !== "admin") redirect("/");

  return user;
});

Use it at the top of each admin page, and in each route handler:

// app/admin/page.tsx
import { requireAdmin } from "@/app/lib/dal";

export default async function AdminPage() {
  await requireAdmin();
  // Load and show the admin data here.
}
// app/api/admin/users/route.ts
import { requireAdmin } from "@/app/lib/dal";

export async function GET() {
  await requireAdmin();
  // Return the data here.
}

In a route handler, a redirect is not the best answer for an API client. Return a 401 or a 403 status code in an API route. The Next.js guide shows this two-step check in its route handler example.

The function calls getUser() and does not read the session from the cookie only. A call to getUser() asks the Supabase Auth server to verify the token. A cookie alone can be old or forged.

Next.js also supports a check in its proxy file. The guide calls it an “optimistic” check. It reads only the cookie, so it is fast. The guide says that this check “should not be your only line of defense.” Keep the proxy check as an extra step. Put the real check next to the data.

What about other stacks?

The same rule applies in every stack. The server checks the session and the role before it does any work. For an Express app, use one middleware on the whole admin router:

const adminRouter = express.Router();

adminRouter.use(async (req, res, next) => {
  const user = await getUserFromSession(req); // your auth library
  if (!user) return res.status(401).json({ error: "Unauthorized" });
  if (user.role !== "admin") return res.status(403).json({ error: "Forbidden" });
  next();
});

adminRouter.get("/users", listUsers);
app.use("/admin", adminRouter);

Register the middleware before the routes. A route that you add later to adminRouter is then closed by default.

If your app is a static site or a single-page app with no server, you cannot protect pages with browser code. Put the data behind an API or a database with access rules. Then the page can load for a stranger, but it shows no data.

Where must I store the admin role?

Store the role where the user cannot change it. If a user can edit the role, the check is only a suggestion.

For Supabase, the Row Level Security documentation says that a policy that relies on the user_metadata claim “can create security issues” because signed-in users can change this data. It names app_metadata as the place for data that users cannot change, such as team membership.

Set the role from the SQL editor. Use the dashboard, a trusted server, or a migration, and never the browser:

update auth.users
set raw_app_meta_data = raw_app_meta_data || '{"role": "admin"}'::jsonb
where email = 'you@your-company.example';

The documentation also says that the claims in auth.jwt() come from the token as the server issued it. A change to app_metadata appears in a policy check only after the user’s token is refreshed. The user must sign in again, or the app must refresh the session.

Do not use any of these as the source of the role:

  • A value in localStorage or sessionStorage
  • A cookie that the app sets in the browser, such as isAdmin=true
  • A field in a request body or a query string, such as ?admin=1
  • The user_metadata field

If the role is a column in your own table, protect the column. A user must not be able to update their own role. The SaaS privilege escalation guide explains how this fails and how to close it.

How do I protect the data behind the page?

A route check protects one door. The data often has other doors. Close the data too.

For Supabase, enable Row Level Security on each table that the admin page reads. Then allow admin access with a policy. This example gives admins read access to a customers table:

alter table public.customers enable row level security;

create policy "admins read customers"
on public.customers for select to authenticated
using ((select auth.jwt() -> 'app_metadata' ->> 'role') = 'admin');

With no other policy, normal users and anonymous callers read no rows. The Supabase RLS guide covers the full setup of RLS.

Caution: Do not use the service_role key in the browser to make the admin page work. That key bypasses RLS. If the browser has it, every visitor has full access. Read the service_role key guide if you find it in your code.

If your admin actions need the service_role key, run them only in server code, and only after the admin check.

How do I check if someone used the open page?

Read your logs for requests to admin paths from people who are not admins. Start with the host and the database.

  1. Read the access logs of your host. Look for requests to /admin, /api/admin, and the other admin paths. Look for 200 responses to addresses that are not your own.
  2. Look for strange methods. A POST, PUT, PATCH, or DELETE to an admin route from an unknown IP address is the most serious sign.
  3. Look for scanning. Many 404 responses for names such as /admin, /wp-admin, /backup, and /.env in a short time show that a bot looked around. A 200 after that list shows that a path was open.
  4. Check the data for changes you did not make. Look at updated_at and created_at columns. Look for new admin users, changed roles, changed prices, changed emails, and deleted rows.
  5. Check the user table. Look for accounts that you do not know, and for accounts that have the admin role.
  6. Check the exports. If the page has an export function, check if it ran. Treat the exported data as copied.
  7. Check the retention of your logs. Many hosts keep logs for a short time. If the logs do not go back to the start of the problem, you cannot prove that nobody used the page.

If the page showed personal data, treat the problem as a possible data breach. Ask a lawyer about your duty to tell the users in your region.

If you have no audit log, add one now. An audit log records who did what and when. It helps you answer this question the next time.

How do I stop this problem in the future?

Use these controls so that no new admin route opens by accident.

  1. Put all admin code in one place. Use one folder, one router, and one guard. A route outside the folder is easy to find in a review.
  2. Deny by default. Apply the guard to the whole area, as in the Express example. Do not add a guard route by route.
  3. Write a test. The test requests each admin path without a session and expects 401, 403, or a redirect. Run it in your build.
  4. Review code from AI tools. Search for isAdmin, role ===, and localStorage in client code. Ask if a server check backs each one.
  5. Keep admin tools off the public app. If you can, put admin tools on a separate host that only your team can reach, for example behind a VPN or an IP allowlist. The pattern guide on hosting panels and internal surfaces lists the common exposed panels.
  6. Remove old routes. Delete test pages, debug routes, and old dashboards.
  7. Use an authorization pattern. The authorization patterns guide shows how to organize roles and checks in a clear way.
  8. Scan the live app after each release. Your own tests see the routes that you know. A scan of the live site also finds routes that you forgot.

When should I get help?

Get help in these cases:

  • The logs show write requests from unknown sources to admin routes.
  • The admin page showed personal data, payment data, or secrets, and you do not know who saw them.
  • You find new admin accounts that you did not create.
  • The app has many admin routes and you cannot tell which ones are closed. Ask a security engineer to review the app.
  • You cannot add a server-side check because the app has no server. Ask an engineer to move the data behind an API.

After the fix, repeat the curl tests from the confirm section. Every admin address must return 401, 403, or a redirect, and no admin content.

Common questions

Is it enough to hide the admin link from normal users?
No. A hidden link does not stop a direct request. Anyone can type the address or find it in the JavaScript bundle. The server must check the user on every request.
Is a redirect in React code a real check?
No. The redirect runs in the browser after the server sends the page and the data. A user can turn off JavaScript or call the API directly.
Where must I store the admin role?
Store it where the user cannot change it. In Supabase, use app_metadata or a table that RLS protects. Do not use user_metadata, because signed-in users can edit it.
Is a secret admin URL a good protection?
No. A secret path is only hard to guess. It leaks through logs, links, and browser history. Use a login and a role check.
Do I also need to protect the API routes?
Yes. The admin page is only a screen. The data and the actions come from API routes, server actions, or database calls. Each of them needs its own check.

Find open admin routes

A VibeEval scan requests common admin paths and the API calls behind them without a login, and reports the ones that return pages or data.

14-day free trial · No credit card · Cancel anytime

SCAN YOUR APP →