Next.js Security Checklist for 2026 (Beyond the Defaults)
TL;DR: Next.js ships with sensible defaults, but those defaults will not protect your production app from Server Action data exposure, middleware auth bypasses, environment variable leakage, or RSC boundary mistakes. We have audited and shipped over 50 production Next.js apps -- including HIPAA-regulated builds -- and this is the checklist we actually use before every deployment in 2026.
Why are Next.js defaults not enough?
Next.js gives you automatic output escaping, built-in CSRF protection for Server Actions, and a reasonable fetch cache. That covers maybe 40% of the OWASP Top 10 attack surface. The other 60% is on you.
Here is what the framework does not do out of the box:
- It does not set Content-Security-Policy, Permissions-Policy, or X-Frame-Options headers.
- It does not validate or sanitize Server Action inputs.
- It does not prevent you from leaking secrets through
NEXT_PUBLIC_prefixed variables. - It does not enforce authentication at the middleware layer -- it just gives you the hook.
- It does not audit your
node_modulesfor supply-chain attacks.
We learned this the hard way during a React-to-Next.js migration for a fintech client. Their old CRA app had a mature helmet.js setup. When they moved to Next.js, they assumed the framework handled it. It did not. A penetration test found missing security headers across every route within the first week.
What is the "use server" data-exposure risk with Server Actions?
Server Actions are powerful, but the "use server" directive essentially creates a public HTTP endpoint for every exported function in that file. If you define a helper function in the same file and export it -- even accidentally -- it becomes callable from the client.
Concrete risks:
- Any exported function in a
"use server"file is an API endpoint. If that function queries your database without auth checks, anyone with a browser can call it. - Closures in Server Actions can capture variables from the server scope. Those values get serialized and sent to the client as hidden form fields. We have seen API keys end up in HTML source this way.
- Return values from Server Actions are fully visible in the client. Returning an entire user object when you only need a boolean is a data leak.
What we do on every project:
- Keep
"use server"files minimal -- one action per file when possible. - Never export utility functions from action files.
- Validate every input with zod or valibot at the top of every action.
- Re-check authentication and authorization inside every Server Action. Never trust that middleware already handled it.
The Next.js authentication docs are explicit about this: middleware is for redirects, not for security enforcement. Auth must be verified at the data access layer.
How do you do middleware auth right -- and what is the common bypass?
The most common pattern we see in the wild is a middleware.ts that checks for a session cookie and redirects unauthenticated users. This feels secure. It is not.
The bypass: Next.js middleware runs on the Edge Runtime. It matches routes based on your matcher config in middleware.ts. If your matcher does not cover API routes, Server Actions, or dynamically generated paths, those endpoints are wide open.
We have seen this exact pattern fail:
// This misses /api/* and Server Action endpoints
export const config = {
matcher: ['/dashboard/:path*', '/settings/:path*']
}
Our rules for middleware auth:
- Use middleware for UX redirects only -- not as a security gate.
- Always verify the session inside the route handler, Server Action, or data-access function.
- If you must use middleware for auth, set your matcher to
/((?!_next/static|_next/image|favicon.ico).*)and explicitly allowlist public routes. - Never store role-based authorization logic in middleware alone. It is too easy to miss a path.
When we build Next.js projects with sensitive data, we treat middleware as a convenience layer and put the real auth checks one level deeper.
Are your environment variables leaking?
Next.js has a simple convention: prefix a variable with NEXT_PUBLIC_ and it gets inlined into client-side JavaScript. Every other variable stays on the server.
The problem is that developers routinely put values into NEXT_PUBLIC_ variables that should never be public:
NEXT_PUBLIC_STRIPE_SECRET_KEY-- we have seen this in a real codebase during an audit.NEXT_PUBLIC_DATABASE_URL-- same.NEXT_PUBLIC_INTERNAL_API_KEY-- intended for "internal" use but now shipped to every browser.
Our rules:
- Grep your codebase for
NEXT_PUBLIC_before every deploy. Automate it in CI. - Only publishable/client-safe keys get the prefix (Stripe publishable key, analytics IDs, public API URLs).
- Use a
.env.examplefile with comments explaining which values are public and why. - For WordPress-to-Next.js migrations, audit every wp-config value during the move. WordPress does not have this prefix convention, so teams porting config files often prefix everything out of habit.
How should you configure security headers and CSP?
Next.js does not set security headers by default. You need to configure them in next.config.js (or next.config.ts in 2026) via the headers function, or in your middleware.
At minimum, every production site we ship includes:
- Content-Security-Policy -- this is the big one. A well-configured CSP prevents XSS, inline script injection, and data exfiltration. The MDN CSP documentation is the best reference for directive syntax.
- Strict-Transport-Security --
max-age=63072000; includeSubDomains; preload - X-Content-Type-Options --
nosniff - X-Frame-Options --
DENY(unless you specifically need iframing) - Referrer-Policy --
strict-origin-when-cross-origin - Permissions-Policy -- disable camera, microphone, geolocation unless needed.
CSP gotchas in Next.js:
- Next.js injects inline scripts for hydration. You need to use a nonce-based CSP, not
unsafe-inline. Next.js 14+ supports this via thenonceprop on<Script>and thecontentSecurityPolicyexperimental config. - If you use
next/image, your CSPimg-srcmust include your image optimization domains. - Third-party scripts (analytics, chat widgets, ad pixels) will break with a strict CSP. Audit them individually and add specific domain exceptions.
We run our technical SEO audits alongside security header checks because misconfigured CSP can block critical rendering resources and tank Core Web Vitals.
How do you manage dependency and supply-chain risk?
Your Next.js app probably has 800 to 1,200 packages in node_modules. Any one of them can be compromised.
What we do:
- Run
npm auditorpnpm auditin CI. Block deployments on critical/high findings. - Pin exact dependency versions in
package.json(no^or~). Use a lockfile and commit it. - Enable GitHub Dependabot or Renovate with auto-merge disabled -- a human reviews every update.
- Audit new dependencies before adding them. Check download counts, maintenance status, and whether the package has changed ownership recently.
- Use
--ignore-scriptsduring install in CI to prevent postinstall supply-chain attacks.
The OWASP Top 10 now includes "Vulnerable and Outdated Components" as A06. This is not theoretical -- the event-stream and ua-parser-js incidents proved that.
What are the App Router-specific gotchas for RSC data leakage?
React Server Components introduce a new trust boundary. The server component tree renders on the server, but any props passed to a "use client" component are serialized and sent to the browser.
Common mistakes:
- Fetching a full user record in a Server Component, then passing it as a prop to a Client Component that only needs the user's name. The entire record -- email, hashed password, internal IDs -- ships to the client.
- Using
console.login Server Components during development and assuming those logs stay on the server. They do -- but the data you pass across the boundary does not. - Returning sensitive data from Route Handlers without checking the
OriginorRefererheader, creating an open API for scraping.
Our approach:
- Create explicit DTOs (Data Transfer Objects) for every server-to-client boundary crossing. Never pass raw database models.
- Use TypeScript's
OmitorPicktypes to enforce which fields cross the boundary at compile time. - Treat every
"use client"directive as a data checkpoint in code review.
The deployable checklist
Copy this into your project wiki or CI pipeline:
- Every Server Action validates input with zod/valibot
- Every Server Action re-verifies auth and authorization
- No exported helper functions in
"use server"files - Middleware matcher covers all routes (or auth is enforced at the data layer)
-
NEXT_PUBLIC_variables audited -- no secrets exposed -
.env.exampledocuments every variable with public/private annotation - CSP configured with nonce-based inline script policy
- HSTS, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy all set
-
npm auditruns in CI and blocks on critical/high - Dependencies pinned to exact versions, lockfile committed
- No raw database models passed as props to Client Components
- DTOs defined for every server-to-client boundary
- Third-party scripts audited and added to CSP allowlist individually
- Penetration test scheduled for pre-launch (not post-launch)
Frequently asked questions
Is Next.js middleware secure enough for authentication? No. Middleware runs on the Edge Runtime and only covers routes matched by your config. It is best used for redirects and UX logic. Always verify authentication inside Server Actions, Route Handlers, and data-access functions. The Next.js docs say the same thing -- middleware is not a security boundary.
Do I need a Content-Security-Policy if my site has no user input? Yes. CSP protects against more than just stored XSS. It blocks injected scripts from compromised third-party tags, browser extensions, and man-in-the-middle attacks. Even a static marketing site benefits from a strict nonce-based CSP with explicit domain allowlists for fonts, images, and analytics.
How often should we audit Next.js dependencies for security issues? We run automated audits on every pull request and do a manual review of new or updated packages weekly. Critical vulnerabilities should block deployment immediately. At minimum, enable Dependabot or Renovate and ensure a human reviews each update before merging -- automated merges defeat the purpose.