Edge Kit launch!-$50 off
home

Next.js Security Checklist 2026: Auth, Server Actions, CVEs

ยท26 min read

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

  1. 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.
  2. Never use proxy.ts (formerly middleware.ts) as your only auth check. Use it for optimistic redirects, then verify the session again in the page, action, or API.
  3. Put authorization in a Data Access Layer marked import "server-only" that returns small DTOs, not raw database rows.
  4. Authenticate and authorize inside every Server Action. A page-level check does not protect the actions defined on it.
  5. Validate all input with Zod in actions, Route Handlers, params, and searchParams.
  6. Keep secrets out of NEXT_PUBLIC_ variables and out of Client Component props.
  7. Rely on built-in Server Action CSRF checks, but add your own Origin check to cookie-authenticated Route Handlers.
  8. Ship a CSP. Use nonces generated in proxy.ts for dynamic apps, or a hash-based policy if you need static pages.
  9. Set security headers (HSTS, frame-ancestors, Referrer-Policy, Permissions-Policy, nosniff) and drop X-XSS-Protection.
  10. Lock down the image optimizer: narrow images.remotePatterns, keep dangerouslyAllowLocalIP off.
  11. Rate limit auth and expensive endpoints, with a shared store in serverless.
  12. 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.

WhenAdvisorySeverityWhat it breaksFixed in 16.xFixed in 15.x
Mar 2025CVE-2025-29927CriticalMiddleware auth bypass via x-middleware-subrequest headerNot affected15.2.3 (also 14.2.25, 13.5.9, 12.3.5)
Dec 2025CVE-2025-55182 / CVE-2025-66478 ("React2Shell")Critical (CVSS 10.0)Unauthenticated RCE through the RSC protocol, App Router16.0.715.0.5 to 15.5.7 (per minor)
Dec 2025CVE-2025-55184, CVE-2025-67779, CVE-2025-55183High / MediumRSC denial of service, Server Function source code exposure16.0.1015.5.9 (14.x: 14.2.35)
Jan 2026CVE-2026-23864HighServer Components denial of service16.1.5 (16.0.11)15.5.10
Mar 2026CVE-2026-27978MediumOrigin: null bypassed Server Action CSRF checks16.1.7Not affected
Apr 2026CVE-2026-23869HighServer Components denial of service16.2.315.5.15
May 2026CVE-2026-44575, CVE-2026-44574, CVE-2026-44573, CVE-2026-44581High / MediumThree middleware/proxy auth bypasses, XSS with CSP nonces16.2.515.5.16
May 2026CVE-2026-45109HighIncomplete fix for the prefetch proxy bypass (Turbopack)16.2.615.5.18
Jul 2026July security release: CVE-2026-64642, -64641, -64645, -64649 and 5 moreHigh / MediumProxy bypass (Turbopack + single locale), Server Action DoS, SSRF in rewrites and Server Actions16.2.1115.5.21
Aug 2026August security release: GHSA-2xp9-vwfh-vxw4, CVE-2026-75604CriticalUnauthenticated RCE via AVIF image optimization; RCE on Windows-hosted servers16.3.315.5.24
Sep 2026CVE-2026-94545 (GHSA-vcvr-r3jv-pc5j)CriticalRCE in Node.js next/og ImageResponse with attacker-controlled SVG values16.3.6 (affects 16.2.0+)Not affected
Sep 2026September security release: CVE-2026-94483 and 6 moreHigh / MediumSSRF in image optimization via remotePatterns, SSG/ISR cache poisoning, use cache leaks16.3.815.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

Terminal
# 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=latest

Since 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-subrequest header. If your only authorization check lived in middleware, it was gone.
  • May 2026 brought three more bypasses: crafted .rsc and 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):

proxy.ts
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:

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:

data/projects.ts
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 organizationId or userId sent 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 project and 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/server vs @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 behind experimental.taint: true, which also switches the app directory 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:

app/projects/[id]/actions.ts
"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:

  1. Validate every argument with Zod. Arguments are user input even when your TypeScript types say otherwise.
  2. Authenticate and authorize inside the action or the DAL, against the specific resource.
  3. Return only what the UI needs. Return values are serialized to the client. Returning db.update(...).returning() can leak internal columns.
  4. 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.
  5. Self-hosting on several instances? Set NEXT_SERVER_ACTIONS_ENCRYPTION_KEY (a base64 16, 24, or 32 byte key, for example from openssl rand -base64 32) so every instance decrypts the same closures.
  6. No mutations during render. Logging out on ?logout=1 in a Server Component is a GET-triggered side effect. Use an action.
  7. 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: enforceAuth loads the Better Auth session from request headers, and enforceAdmin, enforceUserPermission, or enforceOrganizationPermission add 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:

app/api/auth/[...all]/route.ts
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 Origin header at all is allowed through with a warning, not rejected (serverActions config). Modern browsers send Origin on cross-site POSTs, and SameSite=Lax cookies 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' to allowedOrigins.

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:

next.config.ts
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:

lib/security/same-origin.ts
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:

SettingWhyWhere
Long random BETTER_AUTH_SECRET, unique per environmentSigns sessions and cookiesHost secrets, never NEXT_PUBLIC_
emailAndPassword.requireEmailVerification: trueStops account squatting on unverified emailsAuth server config
2FA (TOTP, OTP, backup codes) and passkeysCredential stuffing defensetwoFactor and passkey plugins
trustedOrigins allowlistOrigin checks for auth endpoints and OAuth callbacksAuth server config
Redirect allowlist for redirectTo / callbackURLPrevents open redirects after loginLogin page and auth config
Rate limiting with shared storageBrute force and signup abuserateLimit.storage
Correct client IP headerRate limits keyed on real client IPsadvanced.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:

packages/auth/src/server.ts
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:

proxy.ts
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 uses eval for 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:

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 supersedes X-Frame-Options; keep both for older clients.
  • Only add HSTS preload once 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 .env files.

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 by sharp, 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 no remotePatterns, you were not affected.

Practical hardening:

next.config.ts
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.dangerouslyAllowLocalIP off. Next.js 16 blocks optimizing local IPs by default and caps images.maximumRedirects at 3 (version 16 upgrade guide).
  • Keep dangerouslyAllowSVG off unless you also set contentSecurityPolicy and contentDispositionType for images.
  • Never build rewrites() or redirects() 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/og SVG 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:

  • dangerouslySetInnerHTML with user content. Sanitize with a maintained sanitizer such as DOMPurify and an allowlist of tags.
  • User-controlled href and src values. Parse with new URL() and allow only http: and https: to block javascript: URLs.
  • Inline scripts built from user input. CVE-2026-44580 covered XSS in beforeInteractive scripts 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.
components/safe-html.tsx
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-lockfile in CI.
  • Run pnpm audit in CI and fail on high or critical.
  • Let Renovate or Dependabot open patch PRs, and auto-merge patch releases of next, react, and react-dom when 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

Sounds good?Now let's make it real. In minutes.
Try TurboStarter

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.

world map
Community

Connect with like-minded people

Join our community to get feedback, support, and grow together with 1,000+ builders on board, let's ship it!

Join us

Ship your startup everywhere. In minutes.

Don't burn tokens on setup and start building features on day one.

Get TurboStarter