An open API route hands your data to anyone with the URL
The concrete risk is narrow and easy to verify: one of your API routes returns a full JSON payload to a caller who never logged in. No session cookie, no Authorization header, no token. A request to something like /api/users or /api/orders comes back with a 200 and a list of records, and the only thing the caller needed was the path.
This is observable, not hypothetical. You can open your own app in a guest browser session, watch the Network tab, and see whether a route answers with data when there is no logged-in user. If it does, anyone who finds that path gets the same response. The path does not have to be guessed: it is usually sitting in your client bundle, because the frontend calls it.
Two things to separate up front. Authentication is "do we know who is calling." Authorization is "is this caller allowed to have this." An unauthenticated endpoint is the first failure: the route never even asks who is calling before it answers. That is the OWASP API Security class of broken authentication and broken object-level authorization showing up at the same door. What is observable from outside is the response; what the route does internally with permissions is something you confirm by reading the code.
- A route that returns records to a request with no session is the suspicious result.
- A route that returns 401 or 403 to the same request is doing its job.
- An endpoint can be unauthenticated on purpose (a public health check, a marketing form). The risk is the ones that were never meant to be public but answer anyway.
Why AI-built apps ship routes without an auth check
AI code generators build the working path first. Asked to "add an endpoint that lists projects," a generator wires a route handler that queries the database and returns the rows. That handler works on the first request, the UI renders, and the feature looks done. The auth check is a separate concern that the prompt did not name, so it often does not get added.
The trap is that nothing breaks when the guard is missing. An authenticated user hits the route through the app, gets their data, and everything appears correct. The missing check is invisible from inside the app, because the app always sends a logged-in session. It only shows up when someone calls the route directly without one.
- Generated middleware frequently protects the page layer (the routes that render UI) while leaving the API layer open. The pages redirect to login; the data endpoints behind them do not.
- File-based routing makes this worse: every file in pages/api/, server/api/, app/api/, or netlify/functions/ becomes a public HTTP endpoint the moment it exists, whether or not you added a guard. See serverless function exposure in AI-built apps for the routing-default version of this.
- Fast deploy means the public URL is live before anyone enumerates which routes actually require a session. This is the same skipped-review pattern behind auth bypass in AI-generated middleware.
Find your open endpoints from a guest session
You can do this on your own app, with no attack and no third-party target. The goal is to list the routes your frontend calls, then hit them as a caller who is not logged in.
Start with the route list. Open your app in a private or incognito window so there is no session, open DevTools, and go to the Network tab. Filter to Fetch/XHR and click through the app. Every request to your own origin under a path like /api/ is an endpoint worth checking. Note the method (GET, POST) and the path. The client bundle and the Sources tab will reveal more routes than the UI exercises, including ones for features you have not clicked.
Then replay each route without a session. Still in the guest window, request the endpoint directly: type the GET URL into the address bar, or use the Network tab to copy the request and re-send it after deleting the cookie and any Authorization header. Compare what comes back.
- Suspicious result: a 200 with structured data (user fields, email addresses, record counts, internal IDs) to a request carrying no session token and no auth header. That route is unauthenticated.
- Safer result: a 401 or 403, or a redirect to login, for any route that should require a user. A public-by-design route returning its intended public data is also fine.
- Check write methods too, not just GET. A POST or PATCH that succeeds without a session means an anonymous caller can change data, which is usually worse than reading it.
- Test one route per resource, then move on. Routes are guarded individually, so a protected /api/orders tells you nothing about /api/invoices.
Add a server-side guard, and know what it does not cover
The fix is a server-side authentication check that runs before the handler touches data, on every route that is not deliberately public. It has to live on the server: a check in client code is advisory only, because the API answers direct requests that never load your JavaScript.
Reject early. Read the session or token, and if it is missing or invalid, return 401 before any query runs. Apply this through shared middleware or a wrapper so a new route is protected by default rather than each handler remembering to add it. Default to closed: treat every endpoint as requiring auth unless you have an explicit reason to make it public, the same deny-first stance that works for database access.
What this fix does not cover: authentication only confirms there is a valid user. It does not confirm that this user is allowed to read this specific record. A logged-in caller requesting /api/orders/1234 may pass the auth check and still receive another customer's order if the handler does not scope the query to the caller. That is broken object-level authorization, and it needs its own per-route ownership check plus code review. Rate limiting, input validation, and what fields the response includes are also separate jobs.
- Put the auth check on the API layer itself, not only on the page that calls it.
- Cover every HTTP method the route accepts, including POST, PATCH, PUT, and DELETE.
- Make "authenticated by default" the scaffold so added routes inherit the guard.
Adding a login wall in front of a route does not make the route safe. It only moves the question from "is anyone allowed in" to "is this person allowed this row." The second question lives in your query logic and cannot be answered by a check at the door.
Where VibeCodeGuard fits, and what it does not prove
VibeCodeGuard's Launch Check crawls your public routes from a clean session and reports endpoints that return structured data (user fields, record counts, IDs) to a request carrying no session token and no auth header. That is exactly the observable failure in this note: a route answering an anonymous caller with real data. The scan flags those endpoints with severity and fix direction, so an open route shows up in your own check instead of someone else's.
What it does not do is confirm whether a route that requires login also enforces the right authorization. The scan sees the response to an unauthenticated request; it does not log in as two different users and compare what each can reach, and it does not read your handler logic. So broken object-level authorization, tenant isolation, and business rules sit outside what a public-surface scan can prove and need manual review or code reading.
Next step: list every /api/ route your app calls, replay each one in a guest session, and add a server-side auth check to any that answers with data. Then run a focused Launch Check against the public URL before sharing it, as a repeatable pre-launch API audit that catches the route you forgot.
A clean Launch Check means no endpoint handed data to an anonymous caller at scan time. It does not mean every route is correctly authorized for every logged-in user, and it is not a penetration test or a code review. Treat a flagged open endpoint as a launch blocker, and treat a clean result as one signal among several.
> launch check
Scan the public surface before launch.
Get severity, evidence, and practical fix guidance for the checks VibeCodeGuard can run from the outside.