
TL;DR
- React2Shell, tracked as CVE-2025-55182 with a CVSS score of 10.0, let an unauthenticated attacker achieve remote code execution through a single crafted HTTP request.
- Since July 2026, Next.js follows a security release program: roughly once a month, the Next.js blog announces an upcoming release with its date and highest severity.
- As of November 2026, upgrade to the latest patch of your line, 16.4.5 or 15.5.29, because Next.js 14 and older get no security releases.
- Proxy is a routing layer, never the only barrier in front of privileged data, so run independent authorization checks in Route Handlers, Server Components and Server Actions.
- Every exported Server Action is reachable by a direct POST request, so verify authentication and authorization inside each action, or in the DAL it calls.
- Next.js exposes any environment variable prefixed with NEXT_PUBLIC_ to the browser, so store secrets in a secrets manager and read process.env only in the Data Access Layer.
Why Next.js apps are a target for attackers
Next.js has moved from a view-rendering framework to a full-stack application core that handles backend logic, database operations and credentials. Because a Next.js deployment connects untrusted client input with sensitive cloud infrastructure, it has become a high-value target for attackers looking for a first foothold.
For a CTO, that changes who owns the risk. A Next.js app is now part of your backend attack surface, so patch windows, access control and secret handling need the same ownership and review cadence as any API service, not only the front-end team’s attention.
The exposure is large. Palo Alto Networks Unit 42 counted over 968,000 React and Next.js instances in its telemetry, and Wiz Research found vulnerable instances in 39% of cloud environments. Attackers exploit new flaws within hours of disclosure, and LLM-assisted vulnerability research, which the Next.js team names as a reason for its new release schedule, makes discovery faster on both sides.
What React2Shell exposed in Server Components
React2Shell changed how teams evaluate server-side rendering boundaries. Tracked upstream as CVE-2025-55182 with a CVSS score of 10.0, it let an unauthenticated attacker achieve remote code execution through a single crafted HTTP request. The flaw was not in Next.js application logic but in the upstream React Server Components Flight protocol, which Next.js apps on the App Router use by default.
A malicious payload could make the server run arbitrary JavaScript under the host process, with full runtime permissions. In recorded intrusions, attackers stole cloud credential files, harvested API tokens and deployed cryptominers, according to Unit 42 and Wiz.
Next.js tracked its impact as CVE-2025-66478, disclosed on December 3, 2025. Next.js 15.x and 16.x with the App Router were affected; Next.js 13.x, 14.x stable, the Pages Router and the Edge Runtime were not. No configuration workaround exists: the fix is an upgrade, first shipped in 15.0.5, 15.1.9, 15.2.6, 15.3.6, 15.4.8, 15.5.7 and 16.0.7. Next.js also advised rotating every secret of an app that was online and unpatched on December 4, 2025.
On Vercel, the platform deployed WAF rules for all customers and blocks new deployments of vulnerable versions, but Vercel states that upgrading remains the only way to fully secure an application, and older preview deployments can still run vulnerable code.
Follow-up denial-of-service and disclosure flaws
Researchers who examined the React2Shell patch found adjacent flaws in the same protocol. CVE-2025-55183 (medium severity) lets a crafted request return the compiled source code of other Server Functions, which can expose business logic and any secret hardcoded in them. CVE-2025-55184 (high severity) lets an unauthenticated caller trap the server in an infinite deserialization loop, a full denial of service.
The first fix for the loop was incomplete, so a second patch followed under CVE-2025-67779. The complete fix is in Next.js 14.2.35, 15.0.7, 15.1.11, 15.2.8, 15.3.8, 15.4.10, 15.5.9 and 16.0.10, and teams that upgraded once had to upgrade again.
Don’t let a simple config leak your private data.
How the middleware bypass worked and what fixed it
CVE-2025-29927, made public in March 2025 with a CVSS score of 9.1, showed how one routing layer can compromise an entire application. An attacker who added an external x-middleware-subrequest header made the framework skip middleware entirely, so redirects and access rules no longer ran and routes that developers believed were guarded became reachable.
The flaw affected versions before 12.3.5, 13.5.9, 14.2.25 and 15.2.3, and the fix is an upgrade to those releases or later. Self-hosted apps running next start or output: 'standalone' were exposed; Vercel-hosted apps were not, because Vercel runs routing in a separate system. Teams that cannot upgrade at once can strip the header at an edge proxy or WAF.
The lesson outlived the patch. Next.js 16 renamed middleware to Proxy (proxy.ts) and recommends it as a last resort, and the July 2026 release fixed another proxy bypass (CVE-2026-64642) in apps built with Turbopack and a single locale. For when it still fits, see Pagepro’s guide to Next.js middleware.
Middleware still has a job in production apps when it is one of several checks. Pagepro built the front end of the order-management platform for Proofed, a US Inc. 5000 editorial-services company, including a Next.js middleware security layer; the partnership is now in its 7th year.
Authentication and authorization on the server
Treat the browser as a hostile environment. Client-side JavaScript runs in user space, so an attacker can change local state or skip client-rendered guards to reach hidden interfaces. Access control must therefore run on the server, on every route and endpoint that handles sensitive data.
Authentication checks identity and returns HTTP 401 when credentials or a session are missing or invalid. Authorization follows and returns HTTP 403 when an identified user tries an action reserved for another role, such as an admin workflow. Make the chain fail closed, so an unexpected error or a missing parameter denies access instead of exposing data.
Use an established authentication library rather than a homegrown identity system. Mature libraries handle session synchronization, cryptographic verification and token lifecycles, and avoid the subtle mistakes that often break custom implementations. The Next.js authentication guide lists options such as Auth.js (NextAuth.js), Better Auth, Clerk, Auth0 and WorkOS, plus Iron Session and Jose if you only need session management.
Session cookies and token handling
Keep credentials out of client-accessible storage. Never store tokens in localStorage or sessionStorage, where cross-site scripting can read them. Send session identifiers in cookies marked HttpOnly, Secure and an appropriate SameSite policy, and sign and verify every token on the server so the client cannot tamper with it. If you set cookies with NextResponse.cookies.set() in Edge routes, upgrade to 16.4.5 or 15.5.29: before that release, the cookie value could leak through an internal response header that JavaScript can read, even when the cookie was marked HttpOnly.
Set time limits on every session. Pair a short token lifetime with an absolute maximum age (the Next.js guide’s example session lasts seven days) and an inactivity timeout, and choose the values by how sensitive the data is. When a session is renewed, rotate the refresh token, invalidate the old session record and issue a new identifier to prevent replay attacks.
Layered authorization without a single point of failure
A single checkpoint in front of sensitive operations is a single point of failure. Proxy works well for early rewrites, redirecting unauthenticated traffic and optimistic checks, but routing layers can be bypassed, so run independent authorization checks in Route Handlers, Server Components and Server Actions as well.
Every entry point must verify access to the specific resource before it touches application state: re-check the user’s identity and confirm the caller owns or has explicit rights to the object. The Next.js docs add a concrete trap: a Proxy matcher that excludes a path also skips the Server Functions on that path, so a matcher change or a refactor can silently remove Proxy coverage.
Keeping sensitive data out of the browser
Passing raw database records through component trees leaks data. Any prop that a Server Component hands to a Client Component is serialized into the HTML and the RSC payload, where anyone can read it in the network panel. The Next.js data security guide recommends a Data Access Layer (DAL) for new projects: a server-only module, marked with the server-only package, that runs every query, performs authorization checks and returns minimal Data Transfer Objects (DTOs).
Map query results to DTOs with only the fields the page renders, because a full user entity can carry password hashes, internal flags or billing data. As an extra layer, set experimental.taint: true in next.config.js and use experimental_taintObjectReference or experimental_taintUniqueValue, so React throws an error when a tainted value is passed to a Client Component. Taint is an early warning, not a substitute for DTO filtering in the DAL.
With Cache Components, a use cache function cannot read cookies() or headers(). Read the session outside the cached scope and pass the user ID in as an argument: arguments are part of the cache key, so each user gets a separate entry instead of another user’s cached data.
Keep Cache Components patched as well: the November 2026 release fixed flaws where use cache entries could be served to the wrong tenant or user, including when binary arguments were used to tell users apart. Pass plain string IDs as cache arguments and upgrade to 16.4.5 or 15.5.29 or later.
A minimal DAL function checks the viewer and returns a DTO instead of the database record:
// data/user-dto.ts
import 'server-only'
import { getCurrentUser } from './auth'
import { db } from './db'
export async function getProfileDTO(userId: string) {
const viewer = await getCurrentUser()
if (!viewer) throw new Error('Unauthorized')
const user = await db.user.findUnique({ where: { id: userId } })
if (!user || (user.id !== viewer.id && !viewer.isAdmin)) {
throw new Error('Forbidden')
}
// Return only the fields the page renders
return { name: user.name, avatarUrl: user.avatarUrl }
}Securing Server Actions and user input
Every exported Server Action is reachable by a direct POST request, not only through your UI. Next.js encrypts action IDs and removes unused actions from the client bundle, but the July 2026 release fixed a flaw (CVE-2026-64643) that disclosed action endpoint IDs, so treat IDs as public. A page-level authentication check does not extend to the actions on that page: verify authentication and authorization inside each action, or in the DAL it calls.
Never rely on browser-side form validation. It guides legitimate users, while automated tools send raw parameters. Parse every payload on the server with a schema library such as Zod or Joi, for example to confirm an email format and an eight-character minimum password before any backend check, and abort without passing unvetted data further.
This action checks the session, validates its input with a strict schema, confirms ownership and returns only a status:
// app/actions.ts
'use server'
import { z } from 'zod'
import { auth } from '@/lib/auth'
import { db } from '@/lib/db'
const schema = z.object({ postId: z.string().uuid() }).strict()
export async function deletePost(input: unknown) {
const session = await auth()
if (!session?.user) throw new Error('Unauthorized')
const { postId } = schema.parse(input)
const post = await db.post.findUnique({ where: { id: postId } })
if (post?.authorId !== session.user.id) throw new Error('Forbidden')
await db.post.delete({ where: { id: postId } })
return { success: true }
}For CSRF, Server Actions accept only POST and compare the Origin header with the Host (or X-Forwarded-Host) header, aborting on a mismatch. Behind a reverse proxy, list trusted origins in serverActions.allowedOrigins. Route Handlers that change state get no such check, so add CSRF tokens there. Return only what the UI needs from an action, and prefer primitive arguments validated by schemas that reject unknown properties. When you self-host on several instances, set NEXT_SERVER_ACTIONS_ENCRYPTION_KEY so every instance encrypts the variables that actions close over with the same key.
Headers, CSP and transport security
HTTP response headers protect your Next.js application against injection, clickjacking and insecure transport. Define them globally in next.config.js or at an edge proxy, so every page gets the same constraints.
A strict Content Security Policy limits which scripts, styles and origins the browser runs. Avoid wildcards: use a nonce generated per request in Proxy, or hashes for static inline blocks, and add report-to or report-uri so your team sees blocked scripts and misconfigured assets.
A nonce-based policy in proxy.ts, shortened from the Next.js guide, looks like this; Next.js reads the nonce from the header and adds it to its own scripts:
// proxy.ts
import { NextRequest, NextResponse } from 'next/server'
export function proxy(request: NextRequest) {
const nonce = Buffer.from(crypto.randomUUID()).toString('base64')
const csp = [
"default-src 'self'",
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
`style-src 'self' 'nonce-${nonce}'`,
"object-src 'none'",
"base-uri 'self'",
"frame-ancestors 'none'",
].join('; ')
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' },
],
},
],
}The trade-off matters for a CTO: a nonce changes on every request, so every page that uses it must render dynamically, which turns off static generation, ISR and CDN caching for those pages and raises server load and hosting cost. Where that is too expensive, set a policy in next.config.js or use the experimental hash-based Subresource Integrity option, which keeps pages static.
Enforce HTTPS with a Strict-Transport-Security header such as max-age=63072000; includeSubDomains; preload, which makes browsers use HTTPS only for two years and qualifies the domain for preload lists.
On Route Handlers, treat CORS as a cross-origin read control, not an authorization barrier: command-line tools ignore CORS, so endpoints must still verify identity. In production, return generic error messages to clients and log stack traces privately, so runtime details and database structures do not leak.
Hardening image optimization
The /_next/image endpoint fetches and processes images on your server, so it is part of your attack surface. In 2026, Next.js fixed critical remote code execution flaws in image optimization of AVIF files (August) and SVG files on glibc-based Linux (October), server-side request forgery for apps with remotePatterns (September), and a bypass that let a disguised SVG through even with dangerouslyAllowSVG turned off (November). Limit which images the optimizer accepts and keep Next.js and sharp patched.
List remote sources in images.remotePatterns as narrowly as possible. The Image docs warn that an omitted protocol, port, pathname or search acts as a wildcard and lets attackers optimize URLs you did not intend:
// next.config.js
module.exports = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'cdn.example.com',
port: '',
pathname: '/uploads/**',
search: '',
},
],
},
}Leave dangerouslyAllowSVG off. If you must serve SVGs through the optimizer, also set contentDispositionType: 'attachment' and a contentSecurityPolicy that blocks scripts, as the docs recommend. On self-hosted servers, make sure sharp is at 0.35.5 or later, the fixed version for the October flaw.
Secrets, dependencies and build artifacts
Keep credentials out of compiled assets. Next.js exposes any environment variable prefixed with NEXT_PUBLIC_ to the browser, so never give a private key that prefix. Store secrets in a secrets manager and read process.env only in the Data Access Layer. Keep public source maps off in production (productionBrowserSourceMaps is off by default), so file layouts, business logic and hardcoded values stay out of browser tools.
Guard the supply chain too. Commit lock files such as package-lock.json to stop dependency drift between environments, and scan packages continuously with tools like Dependabot or Socket. Pagepro runs Dependabot on the repositories it manages: critical and high alerts are triaged within five business days and production patches ship within 30 days, faster under active exploitation. When an incident or exposed credential is suspected, rotate credentials across every integrated service to cut off persistence.
Rate limiting and abuse protection
Brute-force and automated campaigns hit sensitive endpoints long before anyone notices. Limit request volume on login handlers, password resets and public Server Actions with token-bucket or sliding-window limits in Proxy or a reverse proxy, applied per IP address, per account and globally, so attackers cannot exhaust database connections or test stolen credentials at scale. The Next.js backend guide shows the pattern in a Route Handler, where checkRateLimit wraps a store such as Redis; call the same check at the top of sensitive Server Actions:
// app/api/login/route.ts
import { NextResponse } from 'next/server'
import { checkRateLimit } from '@/lib/rate-limit'
export async function POST(request: Request) {
const { rateLimited } = await checkRateLimit(request)
if (rateLimited) {
return NextResponse.json({ error: 'Rate limit exceeded' }, { status: 429 })
}
// Validate credentials only after the limit check
return new Response(null, { status: 204 })
}Put a CDN or edge gateway in front of your origin for TLS termination, WAF rules and challenges that filter headless bots before they reach the Node.js runtime. On Vercel, the firewall supports rate limiting rules without extra infrastructure. Monitor rate-limit hits, spikes in HTTP 429 responses and unusual geographic traffic, so credential stuffing and denial-of-service attempts show up early.
Staying current with Next.js security releases
Treat framework patching as a scheduled operational duty, not an emergency. Since July 2026, Next.js follows a security release program: roughly once a month, the Next.js blog announces an upcoming release with its date and highest severity, and urgent or actively exploited flaws still get ad-hoc patches.
- July 20, 2026: the first scheduled release, Next.js 16.2.11 and 15.5.21, fixed four high and five medium severity vulnerabilities.
- August 25, 2026: the August release, 16.3.3 and 15.5.24, moved forward a day to fix two critical remote code execution flaws: one in image optimization of AVIF files, one on Windows-hosted servers.
- September 22, 2026: an out-of-band update, 16.3.6, fixed a critical remote code execution flaw in the Node.js
ImageResponseimplementation ofnext/og, caused by upstream dependencies; Next.js 15 was not affected, and 15.5.26 added related hardening. - September 30, 2026: the September release, 16.3.8 and 15.5.27, fixed one high, five medium and one low severity issue, including server-side request forgery in image optimization.
- October 14, 2026: an out-of-band update, 16.4.1 and 15.5.28, fixed two critical remote code execution flaws, in
use cachefunctions and in image optimization with SVG. - November 3, 2026: the latest release, 16.4.5 and 15.5.29, fixed one high and six medium severity vulnerabilities, most of them in Cache Components, where cached results could be served across tenants or users.
That is six security releases between July 20 and November 3, two of them out of band and three with critical remote code execution fixes, so budget for a patch cycle every few weeks rather than a few times a year.
Check the support policy when you plan upgrades: as of November 2026, Next.js 16 is Active LTS, Next.js 15 gets security releases through January 31, 2027, and Next.js 14 and older get none.
Hosting changes your exposure. On Vercel, the Next.js team coordinates firewall rules with hosting partners before patches ship, but you still deploy the upgrade. Self-hosted apps on virtual machines or containers also need patched base images, edge proxies and dependencies such as sharp; see Pagepro’s guide to self-hosting Next.js apps. Report new framework flaws through Vercel’s Open Source Bug Bounty; during React2Shell alone, Vercel paid researchers over $1 million for WAF bypasses.
Spotting a compromise after exploitation
After an unauthenticated remote code execution flaw, attackers move fast to establish persistence, harvest credentials and monetize access. That activity leaves traces in the runtime environment, so monitor system-level behavior, not only application errors.
Key indicators of compromise include:
- Unexpected shells or child processes: a Node.js process that spawns sh, bash or PowerShell points to command injection.
- Unfamiliar outbound connections: calls to unknown IP addresses or mining pools, often from a deployed cryptominer such as XMRig.
- Runtime file changes against a production baseline: modified application bundles, unauthorized cron entries or new binaries in writable directories such as /tmp.
Limit the damage in advance: run containers with a read-only root file system, restrict outbound traffic to known destinations and block process spawning, so an attacker cannot run new binaries or send credentials out.
Build enterprise-grade, bulletproof Next.js apps.
Building a recurring security routine
Make security reviews a habit, not a pre-launch sprint. As an application grows, new admin views, Server Actions and data paths add permissions nobody reviewed. Keep an inventory of Server Components, server-rendered views, Server Actions and internal tools, so staging routes and utilities are never left open without current access controls.
Apply least privilege to database roles, API tokens and cloud permissions. Review session policies, Proxy rules and lock files on a fixed schedule, align upgrades with the monthly Next.js security releases, and use automated integration tests to confirm that missing tokens and malformed payloads fail closed.
If your team needs help with patching, upgrades or a security review of an existing Next.js app, see Pagepro’s Next.js support service.
FAQ
CVE-2025-55182, known as React2Shell, is a critical remote code execution vulnerability in the React Server Components Flight protocol, with a CVSS score of 10.0. An unauthenticated attacker can run arbitrary JavaScript on the server through a single crafted HTTP request and reach backend resources, API tokens and internal networks.
Your app was affected if it uses the App Router on Next.js 15.x or 16.x without the patch, tracked in Next.js as CVE-2025-66478. The first fixed versions were 15.0.5, 15.1.9, 15.2.6, 15.3.6, 15.4.8, 15.5.7 and 16.0.7. Later releases fixed further flaws, so upgrade to the latest patch of your line, 16.4.5 or 15.5.29 as of November 2026.
CVE-2025-29927 is fixed in Next.js 12.3.5, 13.5.9, 14.2.25 and 15.2.3, and in every later build. The flaw let attackers skip middleware by adding an external x-middleware-subrequest header. Vercel-hosted apps were not affected.
Any client-side JavaScript can read localStorage, so a cross-site scripting (XSS) attack can steal tokens stored there. Use HttpOnly, Secure cookies with a proper SameSite policy instead, so scripts cannot read credentials and they travel only over HTTPS.
Build a Data Access Layer (DAL) marked with the server-only package, and map database results to Data Transfer Objects (DTOs) that hold only the public fields a component renders. As an extra safeguard, enable experimental.taint in next.config.js so React throws an error when a tainted value reaches a Client Component.
Look for system-level behavior rather than application errors: Node.js spawning shells such as sh, bash or PowerShell, outbound connections to unknown IP addresses or mining pools, and unauthorized cron entries, modified bundles or new binaries in /tmp.
Middleware, renamed Proxy in Next.js 16, is a routing layer and can be bypassed, as CVE-2025-29927 and CVE-2026-64642 showed. A Proxy matcher can also skip Server Functions on excluded paths. Run independent authorization checks in Route Handlers, Server Components and Server Actions, so resources stay protected if routing fails.
Treat every Server Action as a public POST endpoint. Verify authentication and resource ownership inside the action, parse every payload with a schema library such as Zod or Joi, and return only the fields the UI needs. Next.js checks that the Origin header matches the host; behind a reverse proxy, list trusted origins in serverActions.allowedOrigins.
