The page redirects, but the API still answers
The concrete auth bypass middleware risk is this: a logged-out visitor types your dashboard URL, gets bounced to the login screen, and assumes the data behind it is protected. Meanwhile the API route that feeds that dashboard, something like GET /api/users or GET /api/orders, returns the same data to anyone who calls it directly with no session at all. The redirect protected the page. It did not protect the data.
This is an observable, reproducible split, not a hypothetical. The page-level guard runs in the browser navigation flow; the API route is a separate request that the middleware either never sees or waves through. If the route hands back JSON to a caller with no cookie and no token, that is a missing auth check on the data path, regardless of how locked-down the page looks.
Keep two things separate here. Authentication is whether the caller proved who they are. Authorization is whether that caller is allowed to see this specific record. The bypass in this note is primarily an authentication gap: the route never asks who is calling. Once you fix that, you still have to answer the authorization question separately, and that part is not visible from the outside.
Why AI-built apps land here
Ask an AI builder to "add login and protect the dashboard" and it usually does exactly that and no more. It wires up a middleware matcher that intercepts page paths and redirects unauthenticated users to a sign-in route. The page works, the redirect fires, the demo looks finished. The generated answer optimized for the visible requirement.
The API routes that the dashboard calls are a different surface, and they often fall outside that matcher. A middleware config scoped to page paths will commonly skip API paths, or the generator excludes static assets and the API folder in the same breath without distinguishing them. Either way the route handler runs with no session check inside it, because the model assumed the middleware already handled access.
- The page layer is what the prompt described, so that is what gets guarded; the data layer is implied and gets skipped.
- A middleware matcher pattern is easy to scope too narrowly, and the generated default rarely covers every API path.
- The app works end to end while logged in, so the gap is invisible from inside the product. Nobody clicks the raw API URL during a quick review.
- Fast deploy means the public URL exists before anyone enumerates which routes answer without a session. This is the same authentication middleware gap pattern behind broadly unauthenticated API endpoints.
Enumerate your own routes from the outside
You can confirm this on your own app with no attack and no third-party target. The goal is to call your data routes the way a logged-out stranger would, and watch what comes back.
First, find the routes. Log in normally, open DevTools, and watch the Network tab while you use the dashboard. Note every request the page fires to a data path, for example GET /api/users, GET /api/projects, or any path that returns JSON. Those are the routes worth testing. The client bundle and any source maps will also list fetch paths if you read through them.
Then call each one with no session. Copy the request as curl, or refetch it, but strip the cookies and any Authorization header first. A clean private-browser window with no login is the simplest way to send an unauthenticated request. Compare what you get.
- Suspicious result: the route returns 200 with real JSON rows even though you sent no session cookie and no token. That is the bypass: the data path is not checking authentication.
- Safer result: the route returns 401 Unauthorized or 403 Forbidden, or redirects to login, for every request without a valid session. That is the route doing its own check.
- Repeat per route. A protected /api/users tells you nothing about /api/orders. Each handler guards itself or it does not, so walk the whole list.
- Check the write paths too. A POST or DELETE that succeeds with no session is the same gap on a more dangerous verb.
Move the guard onto the route, then check authorization
The fix is to stop relying on page-level middleware as your only gate and put an authentication check where the data actually lives: inside each API route handler, or in middleware whose matcher genuinely covers the API paths.
Practically, that means every data route reads the session or token, rejects the request with 401 when it is missing or invalid, and only then runs. Server-side validation is the point. A guard that only runs during client navigation is trivial to skip by calling the route directly, so the check has to execute on the server for every request to that path. If you keep middleware as the mechanism, widen the matcher to include the API paths and verify it actually fires on them, rather than trusting the generated pattern.
- Put the authentication check in the route handler, or in middleware proven to cover API paths, not only on page navigation.
- Return 401 for missing or invalid sessions and 403 for valid sessions that lack permission, so the two failures stay distinct.
- Re-run the logged-out enumeration above after the fix and confirm every data route now rejects the unauthenticated call.
Closing the authentication gap is not the whole job. Once a route confirms the caller is logged in, it still has to confirm this caller may see this specific record. A signed-in user calling GET /api/orders/123 for an order that belongs to someone else is an authorization failure, and it survives an authentication fix untouched. That object-level check lives in your handler logic and your data model, and it needs code review, not a public-surface probe.
Where VibeCodeGuard fits, and what it cannot prove
VibeCodeGuard's Launch Check probes public API routes from the outside and flags ones that return data to an unauthenticated caller. If a route hands back rows with no session, that is a direct signal of a missing auth check on the data path, and the scan reports it with severity and fix direction. It tests the same thing a logged-out visitor can reach: does this route answer without a login. For the auth endpoints themselves, see the magic-link security checklist; for the wider open-route surface, see the note on unauthenticated API endpoints.
What it does not do is read your handler logic. It cannot tell whether a route that correctly requires a login also enforces that the caller owns the record they asked for. That object-level authorization, the per-record "is this yours" decision, needs code review of your route handlers and data model. The scan sees reachability, not intent.
Next step: list the data routes your dashboard calls, hit each one from a logged-out browser, and confirm it returns 401 rather than rows. Run a focused Launch Check against the public URL before sharing it, so an open route shows up in your scan and not in someone else's.
A clean Launch Check means the routes it probed did not return data to an anonymous caller. It does not prove your authorization logic is correct, that every route was discovered, or that a logged-in user cannot reach another user's records. Treat a flagged open route as a launch blocker, and treat a clean result as one signal among several, not a guarantee.
> launch check
Scan the public surface before launch.
Get severity, evidence, and practical fix guidance for the checks VibeCodeGuard can run from the outside.