BreachFix
All posts
5 min readBreachFix

A practical security check for a Next.js App Router app

nextjssecurityreact

If your Next.js app uses the App Router, the last year gave you a blunt reminder: framework patching is part of application security.

Late in 2025, the Next.js team published advisories for several vulnerabilities in the React Server Components protocol. One issue could allow remote code execution in affected versions. Follow-up research found denial-of-service and source-code-exposure issues. The first denial-of-service patch was incomplete and needed another fix.

This is not a reason to abandon the App Router. It is a reason to keep a short, repeatable security check around it.

There is also a near-term update to watch. The Next.js team has announced a scheduled security release for July 20, 2026 for the 15.5 and 16.2 release lines. At the time of writing, the detailed advisories are not public. If you use either line, follow the official release rather than guessing at the impact from the advance notice.

First, check the version you actually deploy

The December 2025 Next.js security update lists the patched release for each supported line. For Next.js 14, the listed fix is 14.2.35. For 15 and 16, the fixed version depends on the minor line.

Do not copy a version number from an old article and assume you are current. Check the newest Next.js security advisories and upgrade to the latest patched release in your line.

Start with the lockfile:

npm ls next react react-dom
npm outdated

Then confirm the production build uses the same lockfile. It is surprisingly common to update package.json, forget the lockfile, and deploy the old transitive tree.

The source-code-exposure advisory also explains why secrets should not be hardcoded inside a Server Function. Depending on the build setup, a value written directly in source could end up in compiled output. Read secrets from server-only environment variables at runtime, and rotate them after a possible exposure.

Treat every Server Action as an endpoint

A Server Action feels like a function call when you write it:

"use server";

export async function updateTeam(formData: FormData) {
  // ...
}

Over the network, it is still a request that a user can trigger. The Next.js security guidance says to treat Server Actions with the same care as public-facing API endpoints.

That means the action itself must:

  1. Authenticate the user.
  2. Authorize the specific operation.
  3. Validate every argument.
  4. Return only the data the caller needs.

Do not rely on a hidden button or a redirect in the client.

"use server";

export async function updateTeam(input: unknown) {
  const user = await requireUser();
  const parsed = updateTeamSchema.parse(input);

  const membership = await db.membership.findFirst({
    where: { userId: user.id, teamId: parsed.teamId },
  });

  if (membership?.role !== "owner") {
    throw new Error("Forbidden");
  }

  return db.team.update({
    where: { id: parsed.teamId },
    data: { name: parsed.name },
    select: { id: true, name: true },
  });
}

The important check is not "is somebody logged in?" It is "may this user update this team?"

Route Handlers need the same ownership checks

App Router Route Handlers are ordinary HTTP endpoints. A common bug looks like this:

export async function GET(
  _req: Request,
  { params }: { params: { id: string } }
) {
  await requireUser();
  return Response.json(await db.invoice.findUnique({
    where: { id: params.id },
  }));
}

The endpoint checks authentication but not ownership. Any logged-in user who learns another invoice ID may be able to read it.

Put the ownership constraint in the query:

const user = await requireUser();
const invoice = await db.invoice.findFirst({
  where: { id: params.id, userId: user.id },
});

if (!invoice) return new Response("Not found", { status: 404 });
return Response.json(invoice);

Returning 404 avoids confirming whether another user's object exists.

For multi-tenant apps, constrain by both tenant and user role. Never accept a tenant ID from the browser as proof of membership.

Keep server-only code clearly server-only

The NEXT_PUBLIC_ prefix is a publication mechanism. A value with that prefix is intended for browser code.

Good public values:

  • Analytics site ID
  • Public Supabase publishable key
  • Public booking URL

Values that must stay server-side:

  • Database credentials
  • Supabase secret or service_role key
  • Payment-provider secret key
  • OpenRouter or other model-provider key
  • CRM private-app token
  • Email-provider API key

Keep privileged integrations in server modules. Avoid putting server environment values in a shared configuration object that client components import.

Add this check to CI:

rg "NEXT_PUBLIC_.*(SECRET|TOKEN|PRIVATE|SERVICE_ROLE)" .

It is not a complete secret scanner, but it catches an expensive naming mistake.

Review middleware and cache behavior

Middleware affects many routes and can create a large security boundary. Read it as carefully as an authentication service.

Check:

  • Does the matcher cover the routes you think it covers?
  • Are API routes accidentally skipped?
  • Do redirects preserve untrusted query strings?
  • Does the code trust x-forwarded-host or similar headers from any caller?
  • Can a protected response be cached and served to another user?

Do not use middleware as the only authorization layer. It can be useful for early redirects, but the final data access still needs an authorization check.

For authenticated content, inspect actual response headers. Confirm private responses are not cached publicly by a CDN.

Add boring controls that prevent expensive incidents

A secure Next.js deployment usually has a few unexciting controls:

  • A Content Security Policy that fits the app
  • frame-ancestors 'none' or an explicit allowlist
  • X-Content-Type-Options: nosniff
  • A strict referrer policy
  • Request-size limits on JSON and uploads
  • Rate limits on expensive or abuse-prone endpoints
  • Same-origin checks for browser-only mutation endpoints
  • Redacted production errors
  • Production source maps disabled unless access is controlled

None of these replaces application-level authorization. Together, they reduce the number of easy paths into the app.

The short review

If you only have an hour:

  1. Verify Next.js, React, and React DOM against the latest official advisories.
  2. List every Server Action and mutation Route Handler.
  3. Check authentication, object ownership, role checks, and input validation inside each one.
  4. Search client bundles and NEXT_PUBLIC_ variables for secrets.
  5. Test one user against another user's IDs.
  6. Inspect middleware, security headers, and cache headers in the deployed app.

Framework updates close framework vulnerabilities. They do not close a missing tenant check in your own code. You need both.

Sources

Want us to check your app for this?

We prove what’s exploitable and hand you a clear report. Share your app and we’ll confirm scope.

Start an audit →