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

v0 app API route unprotected: how to find and fix it

FIX / V0 API ROUTE / 9 MIN READ

A Next.js API route that v0 generates is public until the code in the route checks who the caller is, so test each route with curl and add a session check and an owner check to every route that reads or changes data. A login screen does not protect the route. Anyone who knows the URL can call it.

This guide stays on route handlers in the app/api folder. It shows how to find the open routes, how to fix them, and how to check the damage.

What does “v0 API route unprotected” mean?

It means that a request with no login, or with the login of another user, can reach the route and get a result. The result can be data, or a change to data, or a paid call to an outside service.

v0 often generates Next.js code with route handlers. A route handler is a file such as app/api/orders/route.ts. It exports functions named GET, POST, PUT, and DELETE. Each function answers requests at the URL /api/orders.

The URL is public, and the browser is not the only client. The Next.js docs give this rule in the authentication guide: treat Route Handlers with the same security considerations as public-facing API endpoints, and verify that the user is allowed to access the Route Handler.

An AI tool builds the screen first. The screen shows a list of orders, so the tool writes a route that returns the orders. The tool tests the route as a signed-in user. Nobody tests the route as a stranger. The route returns data to both.

There are three types of this bug:

Type What the caller can do
No authentication Call the route with no login and get a result
No authorization Sign in as any user and read or change the records of other users
Open proxy Use a paid key of yours, such as an AI key, through your route

The second type is the most common. A route that takes an id from the URL and returns that record is a typical example. The route checks that a user is signed in, but not that the record belongs to that user. Read broken object-level authorization in AI-generated apps for more on this type.

How do I find the unprotected routes?

List the routes first. Then call each one in two ways: with no login, and with the login of a second user.

Step 1: List all route files

Run this command in the root of the project:

find app -path '*/api/*' -name 'route.*' | sort
find pages/api -type f 2>/dev/null | sort

The first command lists App Router routes. The second command lists older Pages Router routes. Also search for Server Actions, because they are public endpoints too:

grep -rln "use server" app lib 2>/dev/null

The Next.js docs say to treat Server Actions with the same security considerations as public-facing API endpoints. Write down each file. For each exported function, write down what it reads and what it changes.

Step 2: Call each route with no login

Use curl. Replace the host and the paths with your own:

curl -i https://your-app.vercel.app/api/orders
curl -i https://your-app.vercel.app/api/users
curl -i -X POST https://your-app.vercel.app/api/orders \
  -H "Content-Type: application/json" -d '{"item":"test"}'

A safe route returns 401 for no login. A route that returns 200 with data or accepts the POST is open. Check the response body too. Some routes return 200 with an empty list, which is a different case from a refusal. Look at the status code and the body.

Step 3: Call each route as the wrong user

  1. Create two accounts in your app, account A and account B.
  2. As account A, create a record. Note the record ID.
  3. Sign in as account B. Copy the session cookie from DevTools, under Application, then Cookies.
  4. Request the record of account A with the cookie of account B.
curl -i https://your-app.vercel.app/api/orders/ORDER_ID_OF_A \
  -H "Cookie: <session cookie of account B>"

A safe route returns 403 or 404. A route that returns the record is missing the owner check. Repeat the call with PUT and DELETE. Use test records only. Do not paste real cookies into a chat or a ticket.

Step 4: Check for paid calls and secrets

Open each route that calls an outside service. Check if a caller can make the route spend money. A route that forwards a prompt to an AI model and has no auth check is an open proxy. Check if any route returns an environment variable, a key, or a full database row with fields such as a password hash. Read the OpenAI key fix for the paid-key case.

Warning: Run these tests against your own app only. Do not run DELETE calls against records that you cannot lose.

What do I do first? Fix it now, in order of urgency

Do the steps in this order. Order by the damage a route can do.

Caution: A new check can break screens that worked by accident. Test each screen after each change. Do not skip the check to fix a screen.

  1. Close the routes that change data or spend money. These are POST, PUT, PATCH, and DELETE routes, and routes that call paid services. Add the session check from the next section.
  2. Close the routes that return private data. These are routes for users, orders, messages, files, and admin data.
  3. Add the owner check. The session check proves who the caller is. The owner check proves that the record belongs to the caller.
  4. Rotate any secret that a route could return or use. Create a new key at the provider and delete the old one. Then update the value in your host. The Vercel secret rotation guide says to update the variable, redeploy, and then invalidate the old credential. It also says that old deployments keep the old values.
  5. Redeploy. Changes take effect only in a new deployment.
  6. Run the curl tests again. Repeat Steps 2 and 3 on the live URL.
  7. Add input checks and limits. See the sections below.

Note: Do step 4 even if you see no abuse. A route that returned a key could have been called by anyone, and the logs may not show it.

How do I add an auth check to a Next.js route?

Put the check inside each route function. Do not rely on a check somewhere else. The Next.js docs recommend a data access layer: one place that verifies the session and returns the user. Each route calls it first.

Here is a route with a session check and an owner check. The functions getSessionUser and db are placeholders for your own auth library and database client:

// app/api/orders/[id]/route.ts
import { getSessionUser } from "@/lib/auth";
import { db } from "@/lib/db";

export async function GET(
  _req: Request,
  { params }: { params: Promise<{ id: string }> },
) {
  const user = await getSessionUser();
  if (!user) return new Response(null, { status: 401 });

  const { id } = await params;
  const order = await db.order.findFirst({
    where: { id, userId: user.id },
  });
  if (!order) return new Response(null, { status: 404 });

  return Response.json({ id: order.id, item: order.item, total: order.total });
}

Read the code in three parts. First, the route gets the user from the session and returns 401 if there is none. Second, the query filters by both the id and the userId. A record of another user is not found, so the route returns 404. Third, the route returns only the fields that the screen needs.

The owner check must be in the query. Do not fetch the record first and check later in a way that you can forget. The filter where: { id, userId: user.id } makes the check hard to skip.

For routes with roles, add a role check after the session check:

if (user.role !== "admin") return new Response(null, { status: 403 });

The Next.js docs show this two-step form. The route returns 401 when the user is not signed in, and 403 when the user is signed in without the right role.

If your data lives in Supabase, the database can enforce the owner check for you. Use Row Level Security, and pass the user token to the client in the route. Read the authorization patterns guide for the general design.

Why middleware is not enough

Next.js has a file named proxy.ts (in older versions, middleware.ts) that runs before routes. Many AI tools put the auth check only there. The Next.js docs say that the proxy is useful for optimistic checks, and that it should not be your only line of defense. They say that the main security checks must be as close as possible to the data source.

There are two practical reasons. A matcher setting can miss a path, so a new route can be left out. The docs example even excludes api from the matcher. A route that depends on the proxy then has no check at all. Put the check in the route.

How do I limit what a caller can send and receive?

A route that checks the user can still be abused. Limit the input and the output.

  1. Validate the input. Use a schema library such as Zod. Reject the request if the body does not match.
  2. Pick the fields. Do not pass the full request body to the database. Copy only the fields that a user may set. A caller must not set role, userId, or price. See mass assignment.
  3. Return only needed fields. The Next.js docs recommend data transfer objects for this. Do not return a full database row.
  4. Set a size limit. Reject very large bodies and very long strings.
  5. Rate limit the routes. Limit by user and by IP address. Read API abuse protection for patterns.

Here is a short example of an input check:

import * as z from "zod";

const Body = z.object({
  item: z.string().min(1).max(200),
  quantity: z.number().int().min(1).max(100),
});

const parsed = Body.safeParse(await req.json());
if (!parsed.success) return new Response(null, { status: 400 });

Do not rely on CORS to protect a route. CORS is a rule that browsers apply. A script, curl, or another server ignores it. Use CORS to control which sites can read responses in a browser, and use authentication to control who can call.

How do I check the damage after an open route?

Find out which routes were open, and what a caller could do with each one. Then look for signs of use.

  1. List the open routes and methods. Use your results from the curl tests.
  2. Open your host logs. On Vercel, use the logs of the project. Filter by the paths of the open routes. Look for requests with no session, from unknown IP addresses, or in large numbers.
  3. Look for scans of IDs. A caller that reads records by ID produces many requests with changing IDs. Look for sequences.
  4. Check your database. Look for new rows, changed rows, and deleted rows that you did not cause. Look for new users with the admin role.
  5. Check the outside services. If a route called a paid API, read the usage page of the provider. Look for cost that you did not cause.
  6. Check your own data. If the route returned personal data, treat that data as read by others when you cannot prove otherwise.
  7. Rotate secrets. Rotate any key that an open route could return.

Your host may keep logs for a short time only, so export them now. If the logs are gone, you cannot prove that nobody used the route.

How do I stop the next unprotected route?

Use these controls for each new route.

  1. Use a shared helper. Create one function that returns the user or a 401 response. Call it first in each route.
  2. Make the owner check part of the query. Filter by the user ID in the database call.
  3. Write a short rule for the AI tool. Tell v0 or your editor that each route must verify the session and the owner. Tell it to return only needed fields.
  4. Review each generated route. Search each new route.ts for a session check. A route with no check is a defect.
  5. Test with curl before release. Run the no-login test and the wrong-user test for each new route.
  6. Keep secrets in server variables. Do not use the NEXT_PUBLIC_ prefix for a secret. The Next.js environment variable guide states that Next.js inlines these variables into the JavaScript that it sends to the browser. Read environment variables security for more.
  7. Scan the deployed app after each release. A scan calls the real routes the way an attacker does.

For platform notes, read the v0 security guide and the Vercel security guide.

When should I get help?

Get help in these cases:

  • An open route returned personal, health, or payment data. Ask a security engineer or a lawyer about your duty to report a breach.
  • Your logs show many requests to the open route, and you cannot tell what they read. Treat the data as exposed.
  • The route changed money, roles, or permissions. Review the database for changes, and ask an engineer to check the code.
  • Your app has many routes, and you cannot review each one. A scan lists the routes that answer without a login.
  • You find a route that returns a secret. Rotate the secret first, then ask for help to check where the secret was used.

After you finish, run the no-login test and the wrong-user test one more time on the live URL. Confirm that each private route returns 401 or 403 or 404.

Common questions

Is a Next.js API route private by default?
No. A route handler is a public URL. The Next.js docs say to treat Route Handlers with the same security considerations as public-facing API endpoints.
Does the login page protect my API routes?
No. A login page only controls a screen. A person can call the route URL directly, so the route must check the session itself.
Is middleware or proxy enough to protect a route?
No. The Next.js docs say that the proxy should not be your only line of defense. Put the main check close to the data, inside the route.
Does CORS stop other people from calling my route?
No. CORS is a browser rule. A script, curl, or a server can call the route with no CORS check.
Do I need to rotate keys that my route used?
Rotate a key if the route could return it or let a caller use it. If the route only reads your database, review the data first.

Test your API routes

A VibeEval scan finds the API routes of your deployed app, calls them with no login and with a second account, and reports the routes that return or change data.

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

SCAN YOUR APP →