Next.js Security Checklist 2026: Auth, Server Actions, CVEs
Next.js security for 16.x: patched versions for the 2025-2026 CVEs, why proxy.ts is not auth, Server Actions, CSRF, CSP nonces and Better Auth.

Next.js security in 2026 is less about exotic attacks and more about three habits: stay on a patched release, authorize on the server next to the data, and treat every Server Action and Route Handler as a public endpoint. The last 18 months proved it. A middleware authorization bypass (CVE-2025-29927), a CVSS 10.0 remote code execution in React Server Components ("React2Shell"), and five more proxy bypasses in 2026 all punished the same mistakes.
This checklist is written for Next.js 16.x (16.4 is current as of 9 October 2026) with Better Auth, and every version number below links to the official advisory.
Short answer: the Next.js security checklist
- Patch first. Run Next.js 16.3.8 or later (16.4.0 is current) or 15.5.27 on the 15.x line. Watch the Next.js blog for the monthly security release.
- Never use
proxy.ts(formerlymiddleware.ts) as your only auth check. Use it for optimistic redirects, then verify the session again in the page, action, or API. - Put authorization in a Data Access Layer marked
import "server-only"that returns small DTOs, not raw database rows. - Authenticate and authorize inside every Server Action. A page-level check does not protect the actions defined on it.
- Validate all input with Zod in actions, Route Handlers,
params, andsearchParams. - Keep secrets out of
NEXT_PUBLIC_variables and out of Client Component props. - Rely on built-in Server Action CSRF checks, but add your own Origin check to cookie-authenticated Route Handlers.
- Ship a CSP. Use nonces generated in
proxy.tsfor dynamic apps, or a hash-based policy if you need static pages. - Set security headers (HSTS,
frame-ancestors,Referrer-Policy,Permissions-Policy,nosniff) and dropX-XSS-Protection. - Lock down the image optimizer: narrow
images.remotePatterns, keepdangerouslyAllowLocalIPoff. - Rate limit auth and expensive endpoints, with a shared store in serverless.
- Automate dependency updates and rotate secrets after any RCE-class advisory.
Quotable definition: Next.js security is the set of framework settings, code patterns, and patch practices that keep a Next.js app's server-side surface (Server Components, Server Actions, Route Handlers, Proxy, and the image optimizer) from leaking data or executing attacker input. Most of it is your code. The framework provides guardrails, not authorization.
Next.js CVEs you must be patched against (2025-2026)
If you only do one thing after reading this, check your installed version against this table. Ranges come straight from the Next.js GitHub security advisories and the official release posts.
| When | Advisory | Severity | What it breaks | Fixed in 16.x | Fixed in 15.x |
|---|---|---|---|---|---|
| Mar 2025 | CVE-2025-29927 | Critical | Middleware auth bypass via x-middleware-subrequest header | Not affected | 15.2.3 (also 14.2.25, 13.5.9, 12.3.5) |
| Dec 2025 | CVE-2025-55182 / CVE-2025-66478 ("React2Shell") | Critical (CVSS 10.0) | Unauthenticated RCE through the RSC protocol, App Router | 16.0.7 | 15.0.5 to 15.5.7 (per minor) |
| Dec 2025 | CVE-2025-55184, CVE-2025-67779, CVE-2025-55183 | High / Medium | RSC denial of service, Server Function source code exposure | 16.0.10 | 15.5.9 (14.x: 14.2.35) |
| Jan 2026 | CVE-2026-23864 | High | Server Components denial of service | 16.1.5 (16.0.11) | 15.5.10 |
| Mar 2026 | CVE-2026-27978 | Medium | Origin: null bypassed Server Action CSRF checks | 16.1.7 | Not affected |
| Apr 2026 | CVE-2026-23869 | High | Server Components denial of service | 16.2.3 | 15.5.15 |
| May 2026 | CVE-2026-44575, CVE-2026-44574, CVE-2026-44573, CVE-2026-44581 | High / Medium | Three middleware/proxy auth bypasses, XSS with CSP nonces | 16.2.5 | 15.5.16 |
| May 2026 | CVE-2026-45109 | High | Incomplete fix for the prefetch proxy bypass (Turbopack) | 16.2.6 | 15.5.18 |
| Jul 2026 | July security release: CVE-2026-64642, -64641, -64645, -64649 and 5 more | High / Medium | Proxy bypass (Turbopack + single locale), Server Action DoS, SSRF in rewrites and Server Actions | 16.2.11 | 15.5.21 |
| Aug 2026 | August security release: GHSA-2xp9-vwfh-vxw4, CVE-2026-75604 | Critical | Unauthenticated RCE via AVIF image optimization; RCE on Windows-hosted servers | 16.3.3 | 15.5.24 |
| Sep 2026 | CVE-2026-94545 (GHSA-vcvr-r3jv-pc5j) | Critical | RCE in Node.js next/og ImageResponse with attacker-controlled SVG values | 16.3.6 (affects 16.2.0+) | Not affected |
| Sep 2026 | September security release: CVE-2026-94483 and 6 more | High / Medium | SSRF in image optimization via remotePatterns, SSG/ISR cache poisoning, use cache leaks | 16.3.8 | 15.5.27 |
What this means today: on 16.x you want 16.3.8 or newer, and on 15.x 15.5.27. As of 9 October 2026, the GitHub advisory database lists no known advisories for 16.3.8 or 16.4.0. That will change soon: Next.js has announced an out-of-band security update for 14 October 2026 covering two Critical and one High severity issues in upstream dependencies. Plan that upgrade now.
After an RCE advisory, patching is not enough
For React2Shell, the Next.js team recommended rotating every application secret if your app was online and unpatched as of 4 December 2025, 1:00 PM PT (official advisory). Treat any future unauthenticated RCE the same way: upgrade, redeploy, then rotate BETTER_AUTH_SECRET, database credentials, and provider keys.
How to check and stay current
# Which next version is actually installed (not just declared)?
pnpm why next
# Known advisories in your dependency tree
pnpm audit --prod
# Let your coding agent run the upgrade with migration guides and codemods (Next.js 16.4+)
npx next@canary upgrade --agent=latestSince July 2026, Next.js runs a formal security release program: roughly once a month it pre-announces a release with its highest expected severity, then ships patches for the Active LTS (16.x) and Maintenance LTS (15.5.x) lines. Next.js 16.4 also added experimental.agentUpgrade, whose default 'security' policy nudges you during next dev and next build when your installed version has a known vulnerability (Next.js 16.4). Pair that with Renovate or Dependabot so the patch PR exists before you read the advisory.
Why proxy.ts is not an auth layer
Next.js 16 deprecated the middleware file convention and renamed it to Proxy: middleware.ts becomes proxy.ts and the exported function becomes proxy (a codemod handles it: npx @next/codemod@canary middleware-to-proxy .). Proxy runs on the Node.js runtime and cannot be switched to Edge (version 16 upgrade guide). The team's own reasoning for the rename is that "middleware" invited Express-style misuse, and they recommend avoiding it unless no other option exists (proxy docs).
The security history explains why that matters:
- CVE-2025-29927 let anyone skip middleware entirely by sending an internal
x-middleware-subrequestheader. If your only authorization check lived in middleware, it was gone. - May 2026 brought three more bypasses: crafted
.rscand segment-prefetch URLs that missed the matcher (CVE-2026-44575, plus a follow-up fix in CVE-2026-45109), query parameters that changed the dynamic route value the page saw (CVE-2026-44574), and a Pages Router i18n bypass (CVE-2026-44573). - July 2026 added CVE-2026-64642, a proxy bypass for App Router apps built with Turbopack using a single i18n locale.
Every one of those advisories gives the same workaround: enforce authorization in the route, page, or data path instead of relying on middleware. Proxy matches URLs. Your data is not keyed by URL.
Use Proxy for what it is good at: cheap, optimistic redirects. Better Auth's Next.js docs recommend exactly this, and label the cookie check as not secure on its own (Better Auth Next.js integration):
import { getSessionCookie } from "better-auth/cookies";
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function proxy(request: NextRequest) {
// Only checks that a session cookie exists. Real verification happens server-side.
if (!getSessionCookie(request)) {
const url = new URL("/auth/login", request.url);
url.searchParams.set("redirectTo", request.nextUrl.pathname);
return NextResponse.redirect(url);
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard/:path*", "/admin/:path*"],
};Since Proxy now runs on Node.js, you can call auth.api.getSession() there for a full database-backed check. That is fine for UX, but it does not change the rule: the page, Server Action, and API must still verify the session themselves, because a future matcher bug should not be a data breach.
The Data Access Layer: where Next.js authorization belongs
For new projects, the official Next.js data security guide recommends a Data Access Layer (DAL): an internal, server-only module that is the only code allowed to touch the database and process.env, that performs authorization, and that returns minimal Data Transfer Objects (DTOs). It maps directly onto the top risk in the OWASP Top 10:2025, A01 Broken Access Control.
Start with a cached session helper. This mirrors how TurboStarter resolves sessions in apps/web/src/lib/auth/server.ts:
import "server-only";
import { headers } from "next/headers";
import { cache } from "react";
import { auth } from "@/lib/auth";
export const getSession = cache(async () => {
const data = await auth.api.getSession({ headers: await headers() });
return {
session: data?.session ?? null,
user: data?.user ?? null,
};
});cache() deduplicates the lookup within one request, so every layout, page, and DAL function can call getSession() without passing the session around (which is how session objects end up in Client Component props).
Then make every data function authorize and shape its output:
import "server-only";
import { and, eq } from "drizzle-orm";
import { getSession } from "@/lib/auth/server";
import { db } from "@/lib/db";
import { member, project } from "@/lib/db/schema";
async function getAccessibleProject(projectId: string) {
const { user } = await getSession();
if (!user) return null;
const row = await db.query.project.findFirst({
where: eq(project.id, projectId),
});
if (!row) return null;
const membership = await db.query.member.findFirst({
where: and(
eq(member.organizationId, row.organizationId),
eq(member.userId, user.id),
),
});
return membership ? row : null;
}
export async function getProjectDTO(projectId: string) {
const row = await getAccessibleProject(projectId);
if (!row) return null;
return { id: row.id, name: row.name, updatedAt: row.updatedAt };
}
export async function renameProject(input: {
projectId: string;
name: string;
}) {
const row = await getAccessibleProject(input.projectId);
if (!row) return null;
await db
.update(project)
.set({ name: input.name })
.where(eq(project.id, row.id));
return { id: row.id };
}Three details do the security work here:
- Authorization uses the verified session, never an
organizationIdoruserIdsent by the client. That is the Insecure Direct Object Reference (IDOR) bug the Next.js guide calls out. - Not found and forbidden return the same thing, so callers cannot probe which IDs exist.
- The DTO is an allowlist. Add a column to
projectand it does not silently ship to the browser.
server-only, Client Component props, and taint
import "server-only"makes the build fail if a module is imported into client code. Put it in the DAL and in any file that reads secrets.- Everything you pass as a prop to a
"use client"component is serialized into the RSC payload. Nested objects included. Pass DTOs, not database rows or full session objects. TurboStarter's server/client boundaries guide shows the package entry points (@workspace/auth/servervs@workspace/auth/client/web) that enforce this in a monorepo. - React's taint APIs (
experimental_taintObjectReference,experimental_taintUniqueValue) are still experimental in Next.js 16 behindexperimental.taint: true, which also switches theappdirectory to React's experimental channel. The docs warn not to rely on taint as your only protection (taint config). Treat it as a seatbelt, not the brakes.
Securing Server Actions
Every exported Server Action is reachable by a direct POST request, whether or not your UI calls it. Next.js helps a little: action IDs are encrypted and non-deterministic, unused actions are removed from the client bundle at build time, and the request body is capped at 1 MB by default (serverActions.bodySizeLimit). None of that is authorization (data security guide).
The rule from the docs is blunt: a page-level authentication check does not extend to the Server Actions defined within it. Keep actions thin, validate input, and delegate to the DAL so auth lives in one place:
"use server";
import { revalidatePath } from "next/cache";
import * as z from "zod";
import { renameProject } from "@/data/projects";
const renameSchema = z.object({
projectId: z.string().min(1).max(64),
name: z.string().trim().min(1).max(100),
});
export async function renameProjectAction(_: unknown, formData: FormData) {
const parsed = renameSchema.safeParse({
projectId: formData.get("projectId"),
name: formData.get("name"),
});
if (!parsed.success) {
return { ok: false, error: "Invalid input" } as const;
}
const result = await renameProject(parsed.data);
if (!result) {
return { ok: false, error: "Not found" } as const;
}
revalidatePath(`/projects/${result.id}`);
return { ok: true } as const;
}Server Action rules worth printing:
- Validate every argument with Zod. Arguments are user input even when your TypeScript types say otherwise.
- Authenticate and authorize inside the action or the DAL, against the specific resource.
- Return only what the UI needs. Return values are serialized to the client. Returning
db.update(...).returning()can leak internal columns. - Do not close over secrets. Variables captured by an inline
"use server"function are sent to the client encrypted and come back on invocation. Next.js generates a new key per build, but the docs say not to rely on encryption alone. - Self-hosting on several instances? Set
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY(a base64 16, 24, or 32 byte key, for example fromopenssl rand -base64 32) so every instance decrypts the same closures. - No mutations during render. Logging out on
?logout=1in a Server Component is a GET-triggered side effect. Use an action. - Rate limit expensive actions (sending email, AI calls, writes). See the rate limiting section below.
If you are deciding between a Server Action and a Route Handler for a given mutation, our Next.js Route Handlers guide has the decision rule.
Route Handlers and API routes
Route Handlers (app/**/route.ts) get no automatic protection beyond what you write. Apply the same DAL and validation rules, plus a few HTTP-specific ones:
- Authenticate per handler or per router, not with a global matcher. In TurboStarter's API, protected routes compose Hono middleware:
enforceAuthloads the Better Auth session from request headers, andenforceAdmin,enforceUserPermission, orenforceOrganizationPermissionadd authorization on top (access control docs). - Webhooks authenticate by signature, not session. Verify the provider signature (for example
stripe.webhooks.constructEvent) before reading the payload, and use the raw body. - Map errors to safe messages. Log details server-side; return a generic message and status code.
- Validate dynamic segments. The data security guide flags folders with brackets as user input during audits.
Mount Better Auth itself through its handler so you inherit its CSRF and origin checks:
import { toNextJsHandler } from "better-auth/next-js";
import { auth } from "@/lib/auth";
export const { GET, POST } = toNextJsHandler(auth);Does Next.js protect against CSRF?
Partly. Server Actions have built-in CSRF protection; Route Handlers do not.
For Server Actions, Next.js only accepts POST, and compares the host in the Origin header with the app's host (from X-Forwarded-Host or Host). On a mismatch, the action is rejected. Two edges to know, both from the official docs and advisories:
- A request with no
Originheader at all is allowed through with a warning, not rejected (serverActionsconfig). Modern browsers sendOriginon cross-site POSTs, andSameSite=Laxcookies cover most of the rest, but defense in depth matters for sensitive actions. - Before 16.1.7,
Origin: null(sandboxed iframes) was treated as missing and bypassed the check (CVE-2026-27978). Never add'null'toallowedOrigins.
If you sit behind a reverse proxy where the public domain differs from the host Next.js sees, list the safe hosts explicitly instead of disabling checks:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
experimental: {
serverActions: {
allowedOrigins: ["app.example.com", "*.example.com"],
},
},
};
export default nextConfig;For cookie-authenticated Route Handlers that change state, add the same check yourself:
import "server-only";
const allowedOrigins = new Set([process.env.NEXT_PUBLIC_URL]);
export function isSameOrigin(request: Request) {
const origin = request.headers.get("origin");
return origin !== null && allowedOrigins.has(origin);
}Call it at the top of every POST, PUT, PATCH, and DELETE handler that relies on cookies, and return 403 on failure. Better Auth already validates origins for its own endpoints against trustedOrigins; keep that list limited to the web app, mobile scheme, and extension you actually ship.
Authentication hardening with Better Auth
Rolling your own sessions is the riskiest code in most SaaS apps. A maintained library gets cookie flags, token rotation, and CSRF right by default; your job is configuration. If you are still choosing a library, see our Better Auth vs Clerk vs NextAuth vs Supabase Auth comparison.
The settings that matter in production:
| Setting | Why | Where |
|---|---|---|
Long random BETTER_AUTH_SECRET, unique per environment | Signs sessions and cookies | Host secrets, never NEXT_PUBLIC_ |
emailAndPassword.requireEmailVerification: true | Stops account squatting on unverified emails | Auth server config |
| 2FA (TOTP, OTP, backup codes) and passkeys | Credential stuffing defense | twoFactor and passkey plugins |
trustedOrigins allowlist | Origin checks for auth endpoints and OAuth callbacks | Auth server config |
Redirect allowlist for redirectTo / callbackURL | Prevents open redirects after login | Login page and auth config |
| Rate limiting with shared storage | Brute force and signup abuse | rateLimit.storage |
| Correct client IP header | Rate limits keyed on real client IPs | advanced.ipAddress |
Better Auth's built-in rate limiter is on in production with a default of 100 requests per 10 seconds, stricter rules on sensitive paths (for example /sign-in/email and /two-factor/* at 3 requests per 10 seconds), and IPv6 normalization per /64 subnet. Two gotchas from the rate limit docs: the default store is in memory, which does nothing across serverless instances, and it does not trust comma-separated X-Forwarded-For chains, so behind a proxy you should point it at a single trusted header:
import { betterAuth } from "better-auth";
import { nextCookies } from "better-auth/next-js";
export const auth = betterAuth({
// ...database, providers, plugins
emailAndPassword: {
enabled: true,
requireEmailVerification: true,
},
rateLimit: {
storage: "database",
},
advanced: {
ipAddress: {
ipAddressHeaders: ["x-real-ip"],
},
},
plugins: [nextCookies()],
});nextCookies() must be the last plugin; it lets sign-in calls made from Server Actions set cookies. Also note that Better Auth's limiter only covers auth endpoints, and server-side auth.api calls skip it, so your own Server Actions and Route Handlers need separate limits. The Next.js backend-for-frontend guide recommends combining code-level checks with your host's rate limiting or WAF.
TurboStarter ships these flows wired up: email verification, magic links, OAuth, passkeys, and two-factor authentication on top of Better Auth.
How to add a CSP with nonces in Next.js 16
A Content Security Policy is your strongest XSS mitigation. In Next.js 16 the nonce is generated in proxy.ts, passed on the request headers, and Next.js applies it automatically to framework scripts, page bundles, and <Script> components during server rendering. This follows the official CSP guide:
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function proxy(request: NextRequest) {
const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
const isDev = process.env.NODE_ENV === "development";
const csp = `
default-src 'self';
script-src 'self' 'nonce-${nonce}' 'strict-dynamic'${isDev ? " 'unsafe-eval'" : ""};
style-src 'self' 'nonce-${nonce}';
img-src 'self' blob: data:;
font-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
`
.replace(/\s{2,}/g, " ")
.trim();
const requestHeaders = new Headers(request.headers);
requestHeaders.set("x-nonce", nonce);
requestHeaders.set("Content-Security-Policy", csp);
const response = NextResponse.next({ request: { headers: requestHeaders } });
response.headers.set("Content-Security-Policy", csp);
return response;
}
export const config = {
matcher: [
{
source: "/((?!api|_next/static|_next/image|favicon.ico).*)",
missing: [
{ type: "header", key: "next-router-prefetch" },
{ type: "header", key: "purpose", value: "prefetch" },
],
},
],
};Read the nonce in a Server Component with (await headers()).get("x-nonce") and pass it to third-party <Script> tags. Know the tradeoffs before you ship it:
- Nonces require dynamic rendering. Static optimization and ISR are off for those pages, and Partial Prerendering is incompatible with nonce-based CSP. If that cost is too high, the docs offer an experimental hash-based alternative via Subresource Integrity (
experimental.sri) that keeps pages static. 'unsafe-eval'is only for development. React usesevalfor better error stacks in dev; neither React nor Next.js needs it in production.- Patch level matters. CVE-2026-44581 was an XSS that specifically affected App Router apps using CSP nonces; it is fixed in 16.2.5 and 15.5.16.
- Start in report-only mode (
Content-Security-Policy-Report-Only) if you have analytics, payments, or chat widgets, then tighten.
TurboStarter does not force a CSP out of the box because every product adds different third-party scripts; the security checklist covers when to enable one.
Security headers that still matter
Headers that do not need a per-request nonce belong in next.config.ts:
import type { NextConfig } from "next";
const securityHeaders = [
{
key: "Strict-Transport-Security",
value: "max-age=63072000; includeSubDomains; preload",
},
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
{
key: "Permissions-Policy",
value: "camera=(), microphone=(), geolocation=()",
},
{ key: "X-Frame-Options", value: "DENY" },
];
const nextConfig: NextConfig = {
poweredByHeader: false,
async headers() {
return [{ source: "/(.*)", headers: securityHeaders }];
},
};
export default nextConfig;Notes:
- Drop
X-XSS-Protection. MDN marks it deprecated and non-standard, warns it can create XSS vulnerabilities in otherwise safe sites, and recommends CSP instead (MDN). The previous version of this guide recommended it; that advice is outdated. frame-ancestors 'none'in your CSP supersedesX-Frame-Options; keep both for older clients.- Only add HSTS
preloadonce every subdomain serves HTTPS, because preload lists are slow to undo. - Be careful with
Cross-Origin-Embedder-Policy: require-corp: it breaks third-party images and embeds that do not send CORP headers.
Environment variables and secrets
Next.js inlines any NEXT_PUBLIC_ variable into the JavaScript bundle at build time; everything else stays on the server (environment variables guide). That makes the rules simple:
- Never prefix a secret with
NEXT_PUBLIC_. If a key grants privileged access to a third-party API, it is a secret even if it looks like an ID. - Validate env at startup with typed schemas (TurboStarter uses envin presets per package) so a missing or mis-scoped secret fails the build, not production.
- Only the DAL and server-only modules read
process.env. The Next.js docs recommend this explicitly. - Never hardcode secrets in source. CVE-2025-55183 let a crafted request return the compiled source of Server Functions; secrets defined inline in code (instead of read from env at runtime) could be inlined into that output (December 2025 update).
- Rotate after incidents and keep production secrets in your host's secret store, not committed
.envfiles.
The full layout is in TurboStarter's secrets and environment docs.
Image optimizer, rewrites, and SSRF
The image optimizer (/_next/image) fetches remote URLs on your server, which makes it an SSRF and parser-exploit target. It has been the source of two 2026 advisories rated Critical or High:
- AVIF RCE (GHSA-2xp9-vwfh-vxw4, August 2026): a bug in
libheif, used bysharp, allowed unauthenticated RCE when optimizing an attacker-controlled AVIF. Patched releases (16.3.3, 15.5.24) disable AVIF optimization until the upstream fix propagates. - SSRF via
remotePatterns(CVE-2026-94483, September 2026): an allow-listed host whose DNS an attacker controls could point the optimizer at private IPs. If you configure noremotePatterns, you were not affected.
Practical hardening:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "assets.example.com",
pathname: "/uploads/**",
},
],
},
};
export default nextConfig;- Allow exact hosts and paths you control. Avoid broad wildcards over domains where users can host content or change DNS.
- Leave
images.dangerouslyAllowLocalIPoff. Next.js 16 blocks optimizing local IPs by default and capsimages.maximumRedirectsat 3 (version 16 upgrade guide). - Keep
dangerouslyAllowSVGoff unless you also setcontentSecurityPolicyandcontentDispositionTypefor images. - Never build
rewrites()orredirects()destinations from request input. CVE-2026-64645 showed that a hostname built from request data could be pointed anywhere, enabling SSRF or open redirects. - Do not pass untrusted values into
next/ogSVG content without escaping. CVE-2026-94545 turned that into RCE on 16.2.0 to 16.3.5.
XSS: what React escapes and what it does not
React escapes text in JSX, which removes the most common XSS class. The remaining holes are predictable:
dangerouslySetInnerHTMLwith user content. Sanitize with a maintained sanitizer such as DOMPurify and an allowlist of tags.- User-controlled
hrefandsrcvalues. Parse withnew URL()and allow onlyhttp:andhttps:to blockjavascript:URLs. - Inline scripts built from user input. CVE-2026-44580 covered XSS in
beforeInteractivescripts with untrusted input (fixed in 16.2.5). - Markdown and MDX from users. Render with a sanitizing pipeline, never by evaluating MDX from untrusted sources.
import DOMPurify from "isomorphic-dompurify";
interface SafeHtmlProps {
readonly html: string;
}
export const SafeHtml = ({ html }: SafeHtmlProps) => {
const clean = DOMPurify.sanitize(html, {
ALLOWED_TAGS: ["p", "br", "strong", "em", "a", "ul", "ol", "li", "code"],
ALLOWED_ATTR: ["href", "rel"],
});
return <div className="prose" dangerouslySetInnerHTML={{ __html: clean }} />;
};React 19.3, shipped with Next.js 16.4, adds Trusted Types support (Next.js 16.4), which is worth evaluating once your CSP is stable.
Dependency and supply chain hygiene
"Software Supply Chain Failures" is A03 in the OWASP Top 10:2025, and the Next.js table above is your supply chain in practice. Most of the 2026 advisories sat in upstream code (React's RSC protocol, libheif, Satori), not in anything you wrote.
- Commit the lockfile and install with
pnpm install --frozen-lockfilein CI. - Run
pnpm auditin CI and fail on high or critical. - Let Renovate or Dependabot open patch PRs, and auto-merge patch releases of
next,react, andreact-domwhen tests pass. - Subscribe to the Next.js blog and the repository's security advisories so the monthly release does not surprise you.
- Re-test auth paths after upgrades. Several fixes in 2026 changed matcher and routing behavior.
Pre-launch Next.js security audit
Before shipping, walk through these in order. They are ranked by how often they cause real incidents in SaaS apps:
Version and secrets
pnpm why next shows 16.3.8+ (or 15.5.27). No secret has a NEXT_PUBLIC_ prefix. Production secrets live in the host, and none are committed.
Authorization paths
Grep for "use server" and route.ts. Every action and handler validates input, loads the session, and checks ownership or membership. Database imports and process.env appear only in server-only modules.
Proxy assumptions
Delete your proxy.ts mentally. If any page or API would become reachable without login, add the check in the page, layout data function, or handler.
Origins and redirects
trustedOrigins, serverActions.allowedOrigins, and login redirectTo handling only allow hosts and paths you own.
Headers and CSP
curl -I https://your-domain.com shows HSTS, nosniff, Referrer-Policy, and a CSP (report-only is fine for launch week).
Images, uploads, webhooks
remotePatterns is narrow, uploads use presigned URLs with content-type and size limits, and every webhook verifies its signature.
TurboStarter's web security docs cover the same model in more depth for the Core Kit, including organization tenancy and webhook verification.
How TurboStarter applies this
TurboStarter does not rely on Proxy for authorization. Dashboard layouts resolve the Better Auth session on the server and redirect, the API enforces enforceAuth plus permission middleware on every protected route, organization queries are scoped by a verified membership, and server-only code lives behind ./server package entry points so it cannot be imported into client bundles. If you want that wiring without assembling it yourself, the Next.js Better Auth template and Next.js authentication boilerplate pages show what ships in the kit.
It is not magic, though. A starter kit gives you a secure default; patching Next.js monthly and writing authorization for your own resources is still your job.
Frequently asked questions
Only as an optimistic first check. Middleware, now called Proxy in Next.js 16, matches URLs before rendering, and it has been bypassed repeatedly: CVE-2025-29927 in 2025, then CVE-2026-44573, CVE-2026-44574, CVE-2026-44575, CVE-2026-45109, and CVE-2026-64642 in 2026. Each advisory's workaround was to enforce authorization in the page, route, or data layer. Use Proxy for redirects, and verify the session again wherever data is read or changed.
Treat every Server Action as a public POST endpoint. Validate all arguments with Zod, load the session inside the action (or in a server-only Data Access Layer it calls), check that the user owns or can access the specific resource, return only the fields the UI needs, and rate limit expensive actions. A page-level auth check does not protect actions defined on that page.
CVE-2025-29927 is fixed in 15.2.3, 14.2.25, 13.5.9, and 12.3.5; Next.js 16 was never affected. React2Shell (CVE-2025-55182, tracked for Next.js as CVE-2025-66478) is fixed in 16.0.7 and 15.0.5 through 15.5.7 depending on your minor. Because more critical issues followed in 2026, the practical answer is to run 16.3.8 or later (16.4.0 is current) or 15.5.27, and apply the 14 October 2026 update when it lands.
Server Actions, yes: they only accept POST and reject requests whose Origin host does not match the app host, unless you allow it in serverActions.allowedOrigins. Requests with no Origin header pass with a warning, and Origin: null was a bypass before 16.1.7. Route Handlers have no built-in CSRF protection, so add an Origin check and use SameSite cookies for cookie-authenticated state changes. Better Auth applies its own origin checks via trustedOrigins.
Generate a nonce in proxy.ts, set it in both the request headers (with an x-nonce header) and the response Content-Security-Policy header, and Next.js applies the nonce to its own scripts automatically. Pages must render dynamically, and Partial Prerendering is incompatible with nonces. If you need static pages, set a CSP in next.config.ts headers and consider the experimental experimental.sri hash-based approach.
They still sit behind experimental.taint in Next.js 16, which also enables React's experimental channel for the app directory. They are a useful extra guard against passing a whole user object or a secret into a Client Component, but the Next.js docs say not to rely on them as your only protection. Filter data into DTOs in your Data Access Layer first.
It can be. In 2026 it had an unauthenticated RCE via AVIF images (fixed in 16.3.3 and 15.5.24) and an SSRF through allow-listed remotePatterns (fixed in 16.3.8 and 15.5.27). Stay patched, allow only exact hosts and paths you control, and keep dangerouslyAllowLocalIP and dangerouslyAllowSVG off.
The bottom line on Next.js security
Next.js security in 2026 comes down to a short loop: patch every month, keep authorization in a server-only Data Access Layer, and assume every Server Action, Route Handler, and Proxy matcher will be called in ways you did not plan for. The CVE table above shows the framework fixing its side quickly. Your side is the checklist at the top of this page: run it before launch, and again after every feature that touches auth, billing, or file uploads.
Boilerplate vs vibe coding (Claude, ChatGPT Codex, Cursor)
Boilerplate vs vibe coding - when AI-from-scratch works, when a SaaS starter wins, and how to combine both for production.
Connect your Turbostarter (Next.js, React Native, Vite) app to Supabase in 5 minutes
Learn how to connect your TurboStarter monorepo (Next.js, React Native, Vite) to Supabase and use it as your unified database, storage, and edge backend.



