Almost everything specific to Next.js concerns one boundary: what runs on the server versus what ships to the browser. The recurring findings are environment variables published into the client bundle, Server Actions callable without an authorisation check, middleware assumed to protect routes it never matches, and authenticated content cached and served to the wrong user.
Next.js blurs the line between server and client deliberately, which is what makes it productive and also what makes its failure modes distinctive. Code that looks like it runs on the server frequently does not, and the framework will not warn you.
Where Next.js codebases actually break
These are patterns we look for in code produced this way — not flaws in Next.js itself. Each names the check that finds it.
Secrets published through the public environment prefix
Any environment variable carrying the client-visible prefix is inlined into the JavaScript bundle at build time and readable by every visitor. It is also the fastest way to make a value available inside a client component, so it is reached for exactly when someone is trying to get something working. The value is then public permanently, including in every previously deployed build.
Detected by: Hardcoded API key or secret
Server Actions with no authorisation of their own
A Server Action is a callable endpoint, not a private function. Because it is invoked from a component that already sits behind a protected layout, it is easy to assume the protection carries over. It does not: the action can be invoked directly, and any authorisation it needs has to be asserted inside the action itself.
Detected by: Unvalidated request input
Middleware that does not match the route it was written for
Route protection placed in middleware depends entirely on the matcher config. A matcher that misses a path, or a route added later outside its pattern, leaves that route unprotected with nothing in the code indicating it. The protection reads as present in review and is absent at runtime.
Detected by: Unvalidated request input
Authenticated content rendered into a shared cache
Next.js caches aggressively by default. A page that renders per-user data without opting out of static rendering can have one user's response cached and served to the next, which is a data-exposure bug that only appears under real traffic and never in local development.
Detected by: Unvalidated request input
Auditing a Next.js codebase: the short list
List every publicly-prefixed environment variable and confirm none of them is a secret
Call each Server Action directly with no session and confirm it refuses
Read the middleware matcher against the full route tree and find the routes it does not cover
Confirm any route rendering per-user data is explicitly dynamic rather than cached
Check that server-only modules are not imported into client components, which pulls their dependencies into the bundle
Scan your Next.js codebase
Every check named above runs automatically. Free on public repositories, no signup, results in under three minutes — with the file and line for each finding.
Its defaults are reasonable, but the server-client boundary is the thing that catches teams out. Nothing in the framework prevents a secret being published through the public environment prefix, or a Server Action being called without an authorisation check, and neither mistake produces a visible symptom during development.
Do Server Actions need their own auth checks?
Yes. A Server Action is a network-callable endpoint regardless of which component invokes it. Being rendered inside a protected layout confers no protection on the action itself, so every action that touches sensitive data must assert the session and the permission it requires.
How do I know if a secret ended up in the client bundle?
Search the built output rather than the source. Anything inlined at build time will be present as a literal string in the JavaScript served to browsers, and the fastest check is to look for the value itself in the deployed bundle.