Next.js blurs the line between server and client deliberately, which is what makes it pleasant to write and where most of its security problems come from.
Environment variables
Any variable prefixed NEXT_PUBLIC_ is inlined into the client bundle at build time. It is public. Not obscured, not hard to find. A string in JavaScript anyone can read.
Check your deployed bundle rather than trusting the config. View source and search for sk_, secret and key. Hardcoded credentials in client-side code were one of the most common findings across the 24 AI-generated applications we scanned, usually because something needed to work in a preview.
Server components leak through props
A server component can query the database directly, which is the point. The trap is passing the result to a client component.
Everything in user is now in the page payload, password hash included. Select the fields you need, or narrow before you pass.
Server actions are public endpoints
A server action compiles to an HTTP endpoint with a generated identifier. Anyone who finds the identifier can call it directly, with whatever arguments they like, the form you built is not a constraint.
So every server action needs its own authentication and authorisation checks, exactly like an API route:
The ownership check matters as much as the session check. A logged-in user calling the action directly with someone else's post ID is the same broken-authorisation pattern that shows up everywhere else.
Middleware is not your auth layer
Middleware is a reasonable place for redirects and a poor place for your only authorisation check. Its matcher config is easy to get subtly wrong, and the framework has had a bypass at exactly this layer. CVE-2025-29927 let an attacker skip middleware entirely by sending an x-middleware-subrequest header, which Next.js trusted without checking where it came from. It affected every version after 11.1.4 and was patched in 12.3.5, 13.5.9, 14.2.25 and 15.2.3.
The patch closed that particular hole. The lesson generalises: if middleware is the only thing standing between an anonymous request and your data, a framework bug becomes a full authorisation bypass.
Treat it as a convenience layer. Put the real check in the route handler, server action or data access layer, where it runs regardless of how the request arrived.
Everything else
Set security headers in
next.config.js. CSP, HSTS,X-Frame-OptionsRoute handlers need the same five layers as any API endpoint
Keep Next.js itself updated; framework-level auth bypasses have been found and patched more than once
Where this gets hard
Each of these is simple in isolation. The difficulty is that a Next.js app of any size has a lot of server actions, route handlers and data access points, and every one needs the ownership check. Getting it right ninety-nine times and missing once is the same outcome as never doing it.
That's a coverage problem rather than a knowledge problem, and it's why it needs analysis that follows data across the codebase rather than a checklist.