In the App Router, your server surface is route handlers and server actions. Both run on the server, but "runs on the server" is not the same as "is protected." A route handler with no session check is just a public endpoint. Work through the checks below for every handler that reads or writes anything sensitive.

1. Authenticate inside the handler

Don't assume a route is protected because the page that calls it is behind auth. The handler is independently reachable. Resolve and verify the session at the top of every sensitive route handler and server action, and bail out early if it's missing.

  • Call your auth/session check at the start of the handler, not in a layout or the calling component.
  • Remember middleware matchers can miss routes — verify in the handler itself as the source of truth.
  • For server actions, re-check auth inside the action; being rendered on a protected page is not enough.

2. Authorize the specific action and resource

Authentication says who the user is; authorization says whether they may do this. The classic Next.js bug is the ID-based route — /api/projects/[id] — that queries only by the ID from the URL. That's an insecure direct object reference: change the ID, read someone else's data.

Scope ID-based routes by owner to prevent insecure direct object referenceWithout ownership checkwhere id = req.idAny recordReads other users' dataWith ownership checkid = req.id AND owner = meOwn record404 for the rest
Query by the requested ID AND the authenticated owner, and return 404 for everything else.
  1. Scope every query by both the resource ID and the authenticated user or workspace.
  2. Return 404 (not 403) for records the user can't access, so you don't confirm they exist.
  3. Never trust a userId, role or tenantId from the request body — derive it from the session.

3. Validate input at the boundary

AI-generated handlers often read req.json() and pass it straight to the database. Validate the shape and types of every payload with a schema, and reject unexpected fields before they reach your data layer.

const Body = z.object({ title: z.string().min(1).max(200) });
const parsed = Body.safeParse(await req.json());
if (!parsed.success) {
  return Response.json({ error: "Invalid input" }, { status: 400 });
}
  • Use a shared schema (such as Zod) and parse before doing any work.
  • Strip or reject unknown fields rather than spreading the body into a database write.
  • Validate path and query params too, not just the JSON body.

Scan your Next.js app for unprotected routes, IDOR and missing validation automatically.

Audit your API routes

4. Rate-limit and cap expensive routes

Any route that calls an AI model, sends email, accepts uploads, or handles auth needs a limit. Without one, a single script can run up your bill or brute-force a login. Apply per-IP and per-account limits and return a clear 429.

  • Add limits to auth, signup, password reset, upload and AI/generation routes.
  • Set a hard usage ceiling so a loop can't generate a four-figure bill.
  • Be careful reading the client IP behind a proxy — use the platform's trusted header.

5. Constrain CORS and outbound fetches

If you add CORS headers, don't pair a wildcard origin with credentials. And for any handler that fetches a user-supplied URL (webhooks, link previews, imports), you have an SSRF surface.

  • Avoid Access-Control-Allow-Origin: * together with credentials.
  • For URL-fetching routes, block private IP ranges and cloud metadata addresses (169.254.169.254).
  • Allowlist destinations where you can instead of fetching arbitrary URLs.

6. Handle errors without leaking internals

A route that returns a raw exception hands attackers a map of your stack. Catch errors, log the detail privately, and return a generic message with the right status code.

  • Return generic error bodies; keep stack traces and SQL errors out of responses.
  • Log details to a private, access-controlled sink — and scrub secrets and PII from those logs.
  • Use correct status codes (400, 401, 403, 404, 429) so clients behave sensibly.

7. Lock down file uploads and security headers

If any route accepts file uploads, treat every upload as hostile. AI-generated upload handlers tend to accept any file, any size, straight into a public bucket. And the response headers your app sends are a cheap, high-leverage layer most generated apps ship without.

  • Validate file type and size on the server, and store user uploads in a private bucket served via signed URLs.
  • Never trust the client-provided filename or content type — derive and sanitize them server-side.
  • Add baseline security headers (a Content-Security-Policy, X-Content-Type-Options, a sensible Referrer-Policy and HSTS).
  • Don't reflect user input into responses without encoding it.

Run these checks on every route handler and server action and you've closed the large majority of real, exploitable API issues in a Next.js app. The work is repetitive, which makes it a good fit for an automated pass before launch.

Get a route-by-route report of your Next.js API surface.

Scan your repo for free