A Next.js security checklist that fits on one page
Server Actions, the renamed proxy file, and public environment variables each have a default that surprises people. What to verify before you launch.

A Next.js security checklist is mostly a list of defaults, because almost none of the framework's sharp edges are bugs. They are documented behaviours that read as safer than they are. Four of them account for most of what goes wrong, and one of them changed name in Next.js 16, which is worth knowing before you copy advice written for an older version.
Server Actions are public POST endpoints
This is the most important item on the list. A Server Action is reachable by direct POST request, whether or not the component that calls it ever renders for a given user. Next.js generates encrypted, non-deterministic action IDs and strips unused actions from the bundle at build time, which raises the bar. It does not change the model. The framework's own guidance is explicit that you should still treat actions as reachable and verify inside each one.
The failure mode that catches people is assuming a page-level check covers the actions defined on that page. It does not:
export default async function AdminPage() {
const session = await auth()
if (!session?.user?.isAdmin) redirect('/login') // controls the UI only
return (
<form
action={async () => {
'use server'
const session = await auth() // and this controls the action
if (!session?.user?.isAdmin) throw new Error('Unauthorized')
await db.record.deleteMany()
}}
>
<button>Delete records</button>
</form>
)
}
The redirect decides what gets rendered. The action is a separate entry point and has to authorise the caller itself. Then check authorisation as well as authentication: being logged in is not permission to act on a specific row.
middleware is now proxy, and it was never an auth boundary
Next.js 16 deprecates the middleware filename and renames it to proxy, to
make the network-and-routing role clearer. The migration is mechanical:
mv middleware.ts proxy.ts
The named export renames too, from middleware to proxy, and config flags
follow: skipMiddlewareUrlNormalize becomes skipProxyUrlNormalize.
The security point survives the rename. The Next.js documentation warns that a matcher change, or a refactor that moves a Server Function to a different route, can silently remove proxy coverage, and that you should verify authentication and authorisation inside each function rather than relying on the proxy alone.
If you want a reason to take that seriously,
CVE-2025-29927 is it. Next.js
trusted an internal x-middleware-subrequest header to prevent middleware
loops. Sending that header yourself caused the middleware to be skipped
entirely, bypassing every check it contained. It affected versions before
12.3.5, 13.5.9, 14.2.25 and 15.2.3. An authorisation layer that a single request
header can switch off was never a boundary, and the fix in your own code is the
same either way: check again where the work happens.
Anything prefixed NEXT_PUBLIC_ is published
The prefix is not a hint about intent. It is an instruction to inline the value into the client bundle at build time, and Next.js replaces unprefixed variables with an empty string on the client so they cannot leak by accident.
The practical consequence is that adding the prefix to make a value "work in the browser" is the moment it stops being a secret. If you have done that with an API key, see your API key is in your JavaScript bundle for how to find out what already shipped.
Treat every dynamic value as attacker-controlled
Route params, searchParams, headers and form data all arrive from the client
and can all be edited. Validate them at the point of use, not at the point you
expected them to come from.
For defence in depth on the data side, Next.js can enable React's taint APIs
through the experimental.taint option, which lets you mark an object or a
specific value as something that must never cross into a Client Component. It is
a good backstop for a whole-user record that should never be serialised to the
browser, and a poor substitute for choosing what to select in the first place.
The short version
- Auth and authorisation inside every Server Action, not just on the page.
- Rename
middleware.tstoproxy.ts, and do not treat it as your auth layer. - Audit every
NEXT_PUBLIC_variable and assume it is public, because it is. - Validate params, headers and form data where you use them.
- Pin a patched Next.js version and keep it patched.
If your app also talks to Supabase from the browser, the database is doing the rest of the work, and four Supabase RLS mistakes that expose every row is the other half of this checklist. For the checks to run against a finished app rather than its source, start with the security checklist nobody runs on a vibecoded app. For the framework-agnostic version, the OWASP Insecure Direct Object Reference prevention cheat sheet is the reference worth reading once properly.
GhostRecon exercises these paths against a running app, including calling actions directly and replaying requests with the wrong session, and verifies what it finds before telling you about it.
New here?
GhostRecon is a free AI pentester. It attacks your live app from the outside in, with no code and no credentials, the same way a real adversary would, then verifies every finding before it reports it.
Download for Mac
