Application & framework security

Next.js security checklist

Use this seven-item checklist on Next.js 14 and 15 App Router apps. Patch dependencies, validate on the server, keep secrets off the client, fail the build if server modules leak, set security headers, centralize authz and request screening, and run the same scanners in CI that you run in the editor.

8 min read
In short: Use this seven-item checklist on Next.js 14 and 15 App Router apps. Patch dependencies, validate on the server, keep secrets off the client, fail the build if server modules leak, set security headers, centralize authz and request screening, and run the same scanners in CI that you run in the editor.

What is the Next.js security checklist?

Use this checklist on Next.js App Router apps, versions 14 through 16. Most items also apply to the Pages Router; the difference is where the code runs, not which threats exist.

React escapes string children, which reduces many XSS bugs. That is one class of attack. Production apps still need dependency hygiene, input validation, secret handling, server-only boundaries, security headers, centralized authz, and editor or CI checks.

Work through these seven topics in order. Each one is a real control, not a slogan.

  • Patch and lock dependencies.
  • Validate and sanitize every untrusted value.
  • Keep secrets off the client and out of the repo.
  • Prevent server code from shipping to the browser.
  • Set security headers on every response.
  • Centralize authentication, authorization, and request screening.
  • Catch issues in the editor and in CI before they ship.

For login-specific controls, see how to secure login pages. For container and host hardening, see self-hosting Next.js in Docker.

How do App Router and Pages Router differ for security?

The App Router (app/) is the default for new Next.js apps and has been since 14. Route Handlers live in app/api/**/route.ts. Server Components and Server Actions run on the server. Client Components run in the browser. A file imported by both sides can leak secrets if you are not explicit.

The Pages Router (pages/) still ships. API routes live in pages/api. Data fetching uses getServerSideProps and getStaticProps. There is no server-only compiler guard unless you add it yourself, and it is easier to pass a secret from a server function into a page prop.

Treat these as hard rules in both routers:

  • Never return a secret, session token, or raw database row from a Route Handler, Server Action, or getServerSideProps unless the client must see it.
  • Prefix only public values with NEXT_PUBLIC_.
  • Put shared server logic in modules that import server-only.
  • Do not treat middleware as your only authorization layer. Middleware can be skipped or bypassed; authorize again in the handler or Server Action. Next.js 16 renamed this file convention to proxy, but nothing about that advice changes. See the Next.js middleware bypass for the CVE that made the point.

This article uses App Router examples. The same rules apply if you still run Pages Router: validate on the server, keep secrets server-side, and screen the request before expensive work.

How do you keep Next.js dependencies safe?

Apply patches on a schedule you actually keep, and out of band for a critical advisory. A lockfile (package-lock.json, pnpm-lock.yaml, or yarn.lock) must be committed so every environment installs the same graph.

  • Turn on Dependabot, Renovate, or an equivalent bot for npm and GitHub Actions.
  • Run npm audit (or your package manager's audit) in CI and fail on known high-severity issues you have not waived.
  • Review new dependencies for unexpected network, filesystem, or install-script access. Socket or a similar capability review helps when a package suddenly gains new permissions.
  • Prefer a small, maintained library over a trivial one-liner package you could write in ten lines.

Falling behind by several major versions turns a single CVE into an emergency upgrade. Stay current on Next.js itself; middleware and Server Action advisories have shipped more than once, including CVE-2025-29927.

How do you validate data in Next.js?

TypeScript checks types at compile time. It does not check that a string is a safe team name, that a JSON body matches your schema, or that a Server Action argument was not forged.

Validate every path, query, header, cookie, form field, and JSON body on the server. Zod and Valibot are common choices. Put the same schema next to the database model when you can, for example with drizzle-zod.

import { z } from "zod";
const createTeamSchema = z.object({
name: z.string().trim().min(2).max(100),
});
export async function POST(req: Request) {
const parsed = createTeamSchema.safeParse(await req.json());
if (!parsed.success) {
return Response.json({ error: "Invalid team name" }, { status: 400 });
}
// persist parsed.data.name
}

Reject unknown fields unless you have a versioned reason to accept them. Parameterize database queries. Do not concatenate user input into SQL.

Avoid dangerouslySetInnerHTML unless a reviewed sanitizer produced the HTML. Prefer React children and innerText over innerHTML. Markdown-to-HTML and JSON-in-HTML are common bypasses; treat their output as untrusted.

How should Next.js environment variables be handled?

Next.js loads .env* files and keeps unprefixed variables on the server. Anything named NEXT_PUBLIC_* is inlined into the client bundle. Anyone can read it.

Do not store database passwords, signing keys, or ARCJET_KEY in a NEXT_PUBLIC_ variable. Prefer a secrets manager over a long-lived value in the process environment. If you must use environment variables in development, keep production secrets in a manager and inject them at runtime.

Scan build artifacts with TruffleHog or Gitleaks. Self-hosted standalone output is a common place for leaked .env files. The Docker guide covers injection at container start.

How do you stop server code leaking to the client?

Server Components, Client Components, Server Actions, and Route Handlers share an import graph. A helper that reads process.env.DATABASE_URL will fail the build or leak if a Client Component imports it.

Install server-only and put this at the top of every module that must never run in the browser:

import "server-only";

If a Client Component imports that module, the build fails. Use that failure. Do not work around it with dynamic imports that hide the leak.

Server Actions are public HTTP endpoints. Authenticate and authorize inside the action. Validate the arguments. Do not rely on the form UI being hidden.

Which security headers should a Next.js app set?

Set these on every HTML and data response:

  • Content-Security-Policy: start with default-src 'self' and add only the origins you need. Evaluate the policy with Google's CSP Evaluator.
  • Strict-Transport-Security: force HTTPS after you terminate TLS.
  • X-Content-Type-Options: nosniff
  • Referrer-Policy: strict-origin-when-cross-origin
  • Permissions-Policy: disable camera, microphone, and geolocation unless you use them.
  • frame-ancestors in CSP (or X-Frame-Options for older browsers) to limit clickjacking.

Do not set X-XSS-Protection. It is deprecated.

You can set headers in next.config.ts, in middleware, or with Nosecone. Nosecone is useful when the CSP is more than a single string.

import { createMiddleware, defaults } from "@nosecone/next";
export default createMiddleware({
...defaults,
contentSecurityPolicy: {
...defaults.contentSecurityPolicy,
directives: {
...defaults.contentSecurityPolicy.directives,
defaultSrc: ["'self'"],
},
},
});

How do you centralize Next.js security checks?

Scatter auth checks across pages and you will miss one. Put authentication, authorization, validation, and request screening in shared modules and call them from every Route Handler and Server Action.

Authentication establishes who the caller is. Authorization decides whether that caller may perform this action on this object. Use Auth.js (formerly NextAuth.js), Clerk, or a session you control. Check the session in the handler, not only in middleware.

For request-level abuse (bots, floods, common injection patterns) run a rule set before the handler does expensive work. This example is @arcjet/next v1 on an App Router Route Handler, not middleware-only createMiddleware():

import arcjet, { detectBot, fixedWindow, shield } from "@arcjet/next";
import { NextResponse } from "next/server";
const aj = arcjet({
key: process.env.ARCJET_KEY!,
rules: [
shield({ mode: "LIVE" }),
detectBot({ mode: "LIVE", allow: [] }),
fixedWindow({ mode: "LIVE", window: "1m", max: 30 }),
],
});
export async function POST(req: Request) {
const decision = await aj.protect(req);
if (decision.isDenied()) {
if (decision.reason.isRateLimit()) {
return NextResponse.json({ error: "Too many requests" }, { status: 429 });
}
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
// Authenticate, authorize, validate, then do the work.
}

detectBot({ allow: [] }) denies every detected bot. Add CATEGORY:SEARCH_ENGINE on public pages if you want crawlers. Use DRY_RUN first when you are unsure of false positives. Login routes need tighter limits; see secure login pages.

How does the editor and CI catch Next.js security bugs?

Turn on ESLint (including security-related rules you actually enforce), TypeScript strict, and secret scanning in the editor. Trunk, Semgrep, Trivy, TruffleHog, and Gitleaks all have editor plugins and CI jobs.

Run the same scanners in CI that you run locally. An editor warning that is not a CI failure will be ignored. Fail the build on secrets in the diff, on known vulnerable dependencies, and on lint rules you have agreed are blocking.

None of these replace a review of authorization and business-logic abuse. They catch the mistakes that are cheap to find automatically.

Frequently asked questions

What is the Next.js security checklist?

Seven controls for Next.js 14 and 15 App Router: patch and lock dependencies, validate untrusted input, keep secrets off the client, prevent server code from shipping to the browser, set security headers, centralize authentication and request screening, and catch issues in the editor and CI.

Does this checklist apply to the Pages Router?

Yes. The threats are the same. App Router uses Route Handlers, Server Actions, and server-only. Pages Router uses pages/api and getServerSideProps. Authorize in the handler in both routers; do not treat middleware as the only check.

Is React XSS protection enough for Next.js?

No. React escapes string children. It does not validate Server Action arguments, stop secret leaks, set CSP, or rate-limit login.

Should I only use Arcjet middleware in Next.js?

No. Call protect() in the Route Handler or Server Action so you can branch on the decision. Middleware can be skipped; authorize again on the server.

Application security in your code

Protect your application with Arcjet

Get rate limits, bot detection, and attack blocking in your request handlers.