Framework audits/Next.js

Next.js Security Audit

What should a Next.js security audit check?

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.

Frequently asked questions

Is Next.js secure by default?

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.

Related reading