Blog / Access Control

Hidden Admin Routes You Forgot to Lock

· Fortus Team · 7 min read

Imagine a curious visitor opens your app, looks at the dashboard URL, and on a hunch changes one word in the address bar from dashboard to admin. Picture this: instead of an error or a login wall, the admin panel loads. Customer list, usage stats, maybe a delete button or two. Nobody hacked anything. They guessed a path your code happily served, because the only thing protecting it was the fact that you never linked to it publicly.

Here is a scenario I keep seeing: an AI scaffold generates user pages and admin pages side by side, the navigation menu hides the admin link from regular users, and the builder assumes that is enough. During development a debug endpoint gets added for convenience and never removed. Months later the route is still live in production, reachable by anyone who can type, with no server-side role check standing in front of it.

Hidden routes are brute-forceable with wordlists, and admin, dashboard, internal, debug, console, and panel are always on the list. The fix is not a cleverer URL. It is a real authorization check on the server for every privileged path.

Why hiding the link never protected anything

Navigation menus are a convenience for legitimate users, not a boundary against curious ones. Browsers expose every route the server will serve, and anyone can type paths directly, follow JavaScript bundles to route definitions, or run a wordlist against your domain in minutes. If the server renders the page or returns the data without verifying the caller's role, the hidden page was public all along.

AI output makes this worse in a predictable way. The model builds the page component and sometimes adds a client-side redirect for non-admin users, something like checking the role in the browser and pushing back to the login screen. That looks like protection during a demo while logged in as an admin, and it passes every happy-path test. But client-side gating is advisory. The data fetch behind it, the API handler under an admin namespace, or the server component itself may still execute without ever asking whether the session belongs to an admin.

Debug endpoints follow the same pattern. They are useful during development, they get left behind because removing them feels like extra work, and they often expose internals with no environment gate at all. A route that was harmless on localhost becomes a liability the moment it deploys.

Where privileged paths live in a typical codebase

Start by listing every route or handler whose path suggests privilege. In a Next.js app that means directories like app admin, pages admin, and API namespaces like api admin, plus middleware matchers that may or may not cover those paths. In Express it means admin routers and the middleware chain mounted in front of them. In Supabase or Firebase projects it means migrations, row-level-security policies, and security rules that distinguish an admin role, plus callable functions that perform admin work.

Then check for the debug surface: paths containing debug, console, internal, staff, panel, or test fixtures, environment flags like DEBUG or ADMIN that may still be enabled, and deployment configs that might expose staging or debug deploys. Search your repository for these terms explicitly rather than relying on memory, because the routes you forgot are the ones that matter most.

For each candidate, read the server path, not the component. Ask whether a role assertion runs before any rendering or data access happens. A correct pattern looks like checking the session role on the server and returning a forbidden response for non-admins. An incorrect pattern is a browser-only redirect, an authentication check that never verifies the admin role, or no check at all.

A concrete self-test you can run in incognito mode

Open an incognito window where you are logged out, and try the obvious guesses against your own production app: admin, dashboard, internal, debug, console, panel, staff, plus your API equivalents like api admin users and api users. Request each one and note what happens. Anything that renders content, returns JSON rows, or behaves differently from a clean login redirect deserves investigation.

Repeat the exercise logged in as a plain non-admin test user if your app has roles. Visit the same admin and debug paths and call the same admin API routes. A correct system returns forbidden or redirects through a server-side decision. A broken one serves the page or the data because the check only existed in the admin's navigation menu.

Finally, inspect one admin API handler end to end. Confirm the handler verifies identity and then verifies the admin role before touching data. Confirm debug routes are either deleted or gated so they cannot run in production. Record every path that fails this test. That list is short, specific, and directly actionable, which is exactly what makes this self-test worth doing before someone else runs it for you.

What a source-file scanner checks for you

A scanner that reads your repository can enumerate these routes far more thoroughly than manual guessing. It can list every admin-like page and handler, trace whether a server-side role check runs before each one, flag API handlers under an admin namespace that authenticate the user but never check the role, and surface debug endpoints with no production environment gate. It can also recognize handled patterns so it does not cry wolf: server-side role assertions on every admin path, middleware matchers that forbid non-admin roles, database policies that deny non-admin rows, and debug code that is genuinely removed or environment-gated all count as passing.

There are limits worth stating plainly. Protections applied outside the repository, such as IP allowlists, VPN-only routing, edge middleware rules, or hosting-level password protection, are not visible in files. If the code shows no role check but such an outer layer might exist, the honest result is a review item rather than a confirmed failure. The scanner reports what the code proves and marks the rest for human confirmation.

The workflow that works is simple: scan the files to get the complete route inventory, enforce server-side admin checks on every privileged path, delete or gate debug endpoints, and re-verify both logged out and as a non-admin user. Hidden routes stop being a gamble the moment every one of them asks who you are and whether you belong there.

Automated Code Review

List every admin route in minutes

Fortus enumerates admin-like pages and handlers, checks for server-side role checks, and flags debug endpoints left live — with the file and line for each one.

or learn more about Fortus →