VibeSecurity

Platform checklist

Next.js security checklist for AI-built apps

Next.js puts server and browser code in one project, and AI tools move freely between the two. That makes it easy to ship a secret to the browser or to protect a page while leaving the action behind it open. By the end of this checklist you will have tested every server entry point from outside and searched your own build for leaks.

By the VibeSecurity team7 min read

Where the trust boundary is

In the App Router, Server Components, server actions and route handlers run on the server and can reach secrets and databases. Client Components run in the browser. The Next.js documentation says Client Components must follow the same security assumptions as code running in the browser, and that environment variables are only available on the server unless they are prefixed with NEXT_PUBLIC_.

The boundary is crossed in two directions. Data goes out when a Server Component passes props to a Client Component, and everything in those props reaches the browser. Requests come in through pages, route handlers and server actions. The documentation says an exported server action is reachable by a direct POST request, not only through your interface. So the question for every entry point is the same: what happens when a stranger calls this directly?

This page covers the framework. For where you host it, see the checklist for your platform, and for the general idea of why browser code cannot enforce access, see our guides on environment variables and client-side secrets.

The checklist

Next.js review areas
AreaWhat to verifyHow to test on your own projectPass condition
Server actionsEach action verifies the session and the ownership of the record inside the action.List files containing use server and read the first lines of every exported function.Every action that reads or changes private data checks the user itself.
Route handlersEach route.ts checks the caller and returns 401 or 403 when it should.Call every API route with curl and no cookie, using the example in Step 2.Private routes refuse anonymous calls.
Proxy or middlewareIt is used for redirects and early checks, not as the only protection.Read proxy.ts or middleware.ts and its matcher, then check that the routes it skips are protected elsewhere.Removing the file would not expose any data, because checks also exist next to the data.
Public variablesNo secret uses the NEXT_PUBLIC_ prefix.Search env files and the built output with the commands in Step 3.Only values that are safe to publish carry the prefix, and no secret string is in .next/static.
Data sent to the clientClient Components receive only the fields they display.Read the props passed from Server to Client Components and view the page source for extra fields.No password hash, token, internal id or other user's data appears in the page payload.
Cross-user accessChanging an id in a URL or action argument does not reach another user's record.Sign in as a second test user and request the first user's record id.The response is a refusal or an empty result.
Input validationParams, search params, form data and headers are validated on the server.Send unexpected and oversized values with curl, bypassing the form.The server rejects them with a clear error.
HeadersA Content Security Policy and other security headers are set on purpose.Run curl -I against your own deployed address and read the response headers.The headers you chose are present on HTML responses.

Step 1: Audit server actions

The Next.js documentation says a page-level authentication check does not extend to the server actions defined within it, and that you should always re-verify inside the action. It lists built-in protections, such as encrypted action ids and removing unused actions from the build, and then says you should still treat server actions as reachable via direct POST requests.

Find every action and read it. The documented pattern is to check authentication first, then authorisation, meaning that this user may act on this specific record. The auth and db helpers below stand in for whatever your project uses.

Find actions, then compare each with this shape
grep -rln "use server" app src

'use server'

import { auth } from '@/lib/auth'
import { db } from '@/lib/db'

export async function deletePost(postId: string) {
  const session = await auth()
  if (!session?.user) {
    throw new Error('Unauthorized')
  }

  const post = await db.post.findUnique({ where: { id: postId } })
  if (!post || post.authorId !== session.user.id) {
    throw new Error('Forbidden')
  }

  await db.post.delete({ where: { id: postId } })
}

Step 2: Call route handlers from outside

The documentation says to treat route handlers with the same security considerations as public-facing API endpoints. List them, then call each one against your own site with no cookie. Follow up with the two-account test: sign in as a second user, copy the first user's record id and request it.

List and probe your own API routes
find app src -name "route.ts" -o -name "route.js"

export SITE="https://your-site.example"

curl -i "$SITE/api/YOUR_ROUTE"
curl -i -X POST "$SITE/api/YOUR_ROUTE" -H "Content-Type: application/json" -d '{}'

Step 3: Search for secrets in the build

Build the project and search the static output, which is what browsers download. Any hit for a secret-shaped string is a finding. Then list the public variables and confirm each is something you are content to publish. Adjust the patterns to your providers.

To stop server-only code being imported into client code by accident, the documentation recommends marking modules with the server-only package, which causes a build error if the module is imported in the client environment.

Build output and env checks
npm run build
grep -rEl "sk_live_|sb_secret_|service_role|BEGIN PRIVATE KEY" .next/static

grep -rn "NEXT_PUBLIC_" .env* 2>/dev/null
git ls-files | grep -E "^\.env"
git log --all -p -S"sk_live_" | head -50

Step 4: Check what you send to the client

The Next.js documentation recommends a data access layer for new projects: a server-only module that performs authorisation checks and returns minimal objects with only the fields the interface needs. It shows the opposite as a warning, where a whole database row is passed to a Client Component and every field is exposed.

AI-generated code often takes the short path and passes the whole row. View the page source or the network responses on a page showing user data and look for fields that are not on screen. Server action return values are sent to the client too, so return a small result instead of the raw record.

Common mistakes

  • Protecting the page and forgetting the server action inside it.
  • Relying on a matcher in proxy or middleware, then adding a new route it does not cover.
  • Returning null from a layout for unauthorised users. The docs say this does not stop nested routes and server actions from being accessed.
  • Putting a secret in a NEXT_PUBLIC_ variable so a Client Component can call an API directly.
  • Trusting a user id, role or price sent from the browser.
  • Passing a full database row as props.
  • Checking that the user is signed in but not that the record belongs to them.

Keep it working

Repeat the use server search and the route probes after every large generated change, since new entry points appear quietly. The documentation's own audit advice is to spend extra time on proxy.ts and route.ts, on use server files and on the props of use client files. Keep two test accounts for regression checks.

Frequently asked questions

Is Next.js secure by default?

Partly. It has sensible defaults, such as keeping environment variables on the server unless they are prefixed with NEXT_PUBLIC_, and built-in protections for server actions. It does not write your authorisation checks. The documentation says to verify authentication and authorisation inside each server action and route handler.

Are Next.js server actions public endpoints?

Yes, treat them that way. The Next.js documentation says an exported server action is reachable via a direct POST request, not just through your application's interface, and that you should treat them with the same security considerations as public-facing API endpoints.

Is middleware enough to protect routes in Next.js?

No. The authentication guide says Proxy, the current name for middleware, should not be your only line of defence and that most checks should happen as close as possible to your data source. Use it for redirects and early filtering, and keep real checks in your data access code.

Are NEXT_PUBLIC_ environment variables safe?

They are public. Next.js exposes any variable with that prefix to the browser, so use it only for values you would be content to publish, such as a publishable key or a public URL. API secrets and database credentials must never carry it.

Does Next.js protect against CSRF?

For server actions, to a degree. The documentation says they only accept POST and that Next.js compares the Origin header to the Host header and aborts the request if they do not match. The documentation describes that check for server actions only, so confirm in your own project how route handlers that change data and rely on cookies are protected.

Sources

  1. 1.Next.js docs: Data security
  2. 2.Next.js docs: Authentication
  3. 3.Next.js docs: Environment variables
  4. 4.Next.js docs: proxy file convention
  5. 5.Next.js docs: Content Security Policy
  6. 6.OWASP Cheat Sheet: Insecure Direct Object Reference Prevention