Quick Answer
Unprotected API routes accept requests from anyone without checking who is calling them. AI code generators like Bolt, Lovable, and Cursor routinely create routes that perform sensitive operations - reading user data, deleting records, sending emails - without any authentication middleware. Adding a single auth check at the middleware layer closes the gap.
Why AI-Generated APIs Skip Authentication
When you prompt an AI tool to "create an API endpoint that returns user orders," it builds the fastest working solution: a route that responds to any HTTP request. Authentication is context that AI models frequently drop because it was not explicit in the prompt and because training examples on the open web are full of tutorial code that skips auth for brevity.
The result is routes like /api/admin/users, /api/delete-account, or /api/send-email sitting wide open in production. According to the OWASP API Security Top 10 (2023), Broken Object Level Authorization and Broken Authentication together account for the top two API security failures. AI-generated codebases amplify this problem because many routes are wired up quickly and never audited.
CodeRabbit's State of AI vs Human Code Generation Report (December 2025) found security issues up to 2.74x higher in AI co-authored pull requests than in human-only ones, with improper password handling and insecure object references the most prominent patterns. A 2021 study of GitHub Copilot, Asleep at the Keyboard?, found that about 40% of 1,689 Copilot-generated programs in high-risk security scenarios were vulnerable.
What an Unprotected Route Looks Like
Here is a typical pattern that Cursor or Bolt generates for a Next.js API route. The business logic is correct, but there is no check that the caller is authenticated or authorized.
// ❌ BAD - No authentication check
// pages/api/admin/users.ts (Next.js Pages Router)
import { db } from '@/lib/db';
export default async function handler(req, res) {
if (req.method === 'GET') {
const users = await db.user.findMany();
return res.status(200).json(users);
}
if (req.method === 'DELETE') {
const { userId } = req.body;
await db.user.delete({ where: { id: userId } });
return res.status(200).json({ success: true });
}
}
This route leaks every user record to any unauthenticated caller and lets anyone delete accounts. Because it lives under /api/admin/, a developer might assume it is protected by some server-level rule - but Next.js and Vercel do not add authentication automatically. The path naming means nothing to the runtime.
The Right Pattern: Middleware-Level Auth
A strong first gate is an auth check in Next.js Proxy (the file was called middleware.ts before Next.js 16), so no request reaches a protected route without a valid session. One change covers every route under a path.
// ✅ GOOD - Auth enforced in Next.js Proxy
// proxy.ts (middleware.ts with a middleware() export before Next.js 16)
import { NextResponse } from 'next/server';
import { getToken } from 'next-auth/jwt';
export async function proxy(req) {
const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET });
if (!token) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
// Optional: role check for admin routes
if (req.nextUrl.pathname.startsWith('/api/admin') && token.role !== 'admin') {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
return NextResponse.next();
}
export const config = {
matcher: ['/api/admin/:path*', '/api/user/:path*'],
};
With this in place, every route matching the pattern is checked before its handler runs. Do not stop there: the Next.js authentication guide says Proxy should not be your only line of defense and recommends verifying the session again inside each route handler, as close to the data as possible. A new route that the matcher misses is exactly the kind of gap AI tools leave behind.
Common Patterns That AI Tools Miss
| Route Pattern | Risk If Unprotected | Correct Fix |
|---|---|---|
GET /api/users |
Exposes all user records | Require auth token; scope to calling user |
DELETE /api/posts/:id |
Anyone can delete any post | Auth + ownership check (user owns post) |
POST /api/admin/invite |
Anyone can invite users or send emails | Auth + role === 'admin' check |
GET /api/export/csv |
Full data dump available without login | Session required + rate limiting |
POST /api/webhook |
Fake events can trigger business logic | HMAC signature verification |
Object-Level Authorization: The Next Layer
Even after adding authentication, AI-generated routes often skip the second check: does this authenticated user own the resource they are requesting? OWASP calls this Broken Object Level Authorization (BOLA), and it is the leading API vulnerability in the 2023 Top 10.
An authenticated user should only be able to read or modify their own records. If AI generates GET /api/orders/:id that returns any order by ID without verifying ownership, any logged-in user can enumerate and read every order in your system simply by iterating the ID parameter.
The fix is always to scope database queries to the authenticated user's identity - for example, adding where: { id: orderId, userId: session.user.id } to any Prisma or Drizzle query that fetches a user-owned resource.
How to Find Unprotected Routes in Your Codebase
Manual auditing works for small projects but breaks down once a Bolt or Lovable scaffold generates dozens of routes. A systematic approach:
- List every route file. In Next.js App Router, every
route.tsfile underapp/api/is a live endpoint. In Express, look forapp.get(),app.post(),router.delete(), and similar calls. - Check each handler for an auth call. Look for
getServerSession,getToken,verifyJWT, or Supabase'sauth.getUser(). If none of these is present and the route touches the database, it is a candidate for exposure. - Test unauthenticated. Use curl or Postman without any Authorization header and call every API route. Any 200 response from a route that should require login is a confirmed finding.
- Run automated scanning. Tools like VibeDoctor's Vibe Check automatically flag Express, Fastify, NestJS, Flask, FastAPI, and Django REST Framework routes that have no auth middleware, with specific file paths and line numbers. It skips paths under
/admin/, which it assumes sit behind global middleware, and it does not read Next.js route handlers yet, so check those by hand with the steps above. Free to sign up.
FAQ
Does Supabase Row Level Security replace route-level auth?
Supabase RLS is a defense-in-depth measure at the database layer - it does not replace authentication at the API layer. If your Next.js or Express routes bypass the Supabase client and use the service role key, RLS policies do not apply. Always check authentication in your route handler or middleware first.
How does Next.js middleware differ from checking auth in each route?
Middleware (called Proxy since Next.js 16) runs before the route handler and can short-circuit the request before any database call is made, and it covers new routes that fall under the same path pattern. Per-route auth checks run inside the handler, next to the data. Use both: Next.js recommends treating Proxy as a first, optimistic check and doing the real authorization in each route handler, because a matcher change or refactor can silently leave a route uncovered.
What about webhook endpoints - should those require auth?
Webhooks from Stripe, GitHub, or other external services cannot carry a user session. Instead, validate an HMAC signature that only the sending service knows. For Stripe webhooks in Node.js, use stripe.webhooks.constructEvent(payload, sig, process.env.STRIPE_WEBHOOK_SECRET). Reject any request where signature verification fails.
Can v0 or Lovable-generated code be trusted to include auth?
Only if you explicitly prompt for it and verify the output. These tools generate auth scaffolding when asked, but they do not audit every subsequent route they create. A common failure mode is that the initial login flow is correct but routes added in later prompts - "now add an admin panel" - arrive without auth checks.
What is the difference between authentication and authorization?
Authentication answers "who is this user?" - typically a JWT or session cookie. Authorization answers "is this user allowed to do this thing?" - a role check or ownership check. AI tools often skip both, but authorization bugs (BOLA/IDOR) are more common in AI-generated code because they require business context the model does not have.