Next.js Security: RSC Vulnerabilities & Defense Guide
Security Deep-Dive · Next.js 15.x/16.x

Next.js Security:
RSC Vulnerabilities & Defense Guide

Critical RCE vulnerabilities in React Server Components. Server Actions that bypass page-level auth. Supply chain attacks. CSP misconfigurations. Here's a practical defense guide for production Next.js.

Oleg Maximov June 13, 2026 18 min read

TL;DR

If you're running Next.js 15.x or 16.x on App Router, your application may be vulnerable to CVE-2025-66478 (RCE, CVSS 10.0). Update to the latest patch in your release line immediately.

Every Server Action must independently authenticate and authorize the user — page-level auth does not protect them. Use the Data Access Layer (DAL) pattern with server-only modules to prevent data leakage from Server Components to the client.

Configure CSP headers (nonces for dynamic sites, SRI for static), run npm audit in CI/CD, and always place a reverse proxy in front of self-hosted deployments.

Section 1: React Server Components — The New Attack Surface

The shift from client-side React to Server Components fundamentally changes the security model. Code that used to run in a sandboxed browser environment now executes on your server, accesses databases directly, and handles secrets. This power comes with a new class of vulnerabilities — some discovered, some still waiting to be found.

1.1 The "React2Shell" Family: CVE-2025-66478 (CVSS 10.0)

In December 2025, the security community learned about a vulnerability in the React Server Components protocol — the wire format that transports RSC data between the server and client. CVE-2025-66478 (and its upstream parent CVE-2025-55182 in React itself) allows an attacker to inject a crafted RSC payload that triggers remote code execution on your Next.js server.

Immediate Action Required

If you were running an unpatched Next.js version as of December 4, 2025 at 1:00 PM PT, Vercel advises that you rotate all secrets — the vulnerability may have been exploited without leaving traces in application logs.

Affected versions:

Fixed versions:

Release Line Minimum Fix All Vulnerabilities
15.0.x 15.0.7 CVE-2025-66478, CVE-2025-55184, CVE-2025-55183
15.1.x 15.1.11 CVE-2025-66478, CVE-2025-55184, CVE-2025-55183
15.2.x 15.2.8 CVE-2025-66478, CVE-2025-55184, CVE-2025-55183
15.3.x 15.3.8 CVE-2025-66478, CVE-2025-55184, CVE-2025-55183
15.4.x 15.4.10 CVE-2025-66478, CVE-2025-55184, CVE-2025-55183
15.5.x 15.5.9 CVE-2025-66478, CVE-2025-55184, CVE-2025-55183
16.0.x 16.0.7 CVE-2025-66478, CVE-2025-55184, CVE-2025-55183

1.2 CVE-2025-55184: Server-Side DoS via RSC

This high-severity vulnerability allows a crafted HTTP request to trigger an infinite loop in the RSC protocol handler. The server process hangs, blocking all subsequent requests until manually restarted. The initial fix was incomplete; the complete remediation came under CVE-2025-67779. If you updated to the earliest patch for your release line, ensure you are on the latest patch — not just the first fix.

1.3 CVE-2025-55183: Server Function Source Code Exposure

A medium-severity but practically dangerous vulnerability: a crafted request can recover the compiled source code of Server Functions. This leaks business logic, internal API structures, and — critically — any secrets that were inlined into the compiled output (as opposed to accessed via process.env at runtime). This is why production secrets must always be runtime-env variables, never compile-time inlined values.

1.4 Preventing Data Leakage Through RSC Props

Beyond protocol-level vulnerabilities, the biggest architectural risk with RSC is data leakage through component props. A Server Component fetches a full database record and passes it to a Client Component. That data, while not visible in the HTML, is embedded in the RSC payload that the client receives.

// ❌ Bad: Full DB record passed to client
// page.tsx (Server Component)
const user = await db.user.findUnique({ where: { id } });
return <UserProfile user={user} />;

// ✅ Good: Minimal DTO
const user = await db.user.findUnique({ where: { id } });
return <UserProfile
  user={{
    name: user.name,
    avatar: user.avatarUrl,
    role: user.role,
  }}
/>;

React Taint APIs

React provides experimental taint APIs that prevent accidental leakage of sensitive data: experimental_taintObjectReference and experimental_taintUniqueValue. Enable them in next.config.js:

// next.config.js
const nextConfig = {
  experimental: {
    taint: true,
  },
};
// lib/db.ts
import { experimental_taintObjectReference } from 'react';

export async function getUser(id) {
  const user = await db.user.findUnique({ where: { id } });
  experimental_taintObjectReference(
    'Do not pass user record to Client Components',
    user
  );
  return user;
}

Once tainted, passing the object to a Client Component triggers a runtime error in development and is stripped in production.

Section 2: Server Actions Security

Server Actions are the most misunderstood security boundary in Next.js. They look like event handlers but behave like API routes — and many developers treat them like the former.

2.1 The Three Immutable Rules

Every Server Action must follow these rules. No exceptions.

// ❌ Wrong: Page-level auth only
export default async function DashboardPage() {
  const session = await getSession(); // redirects if not authenticated
  // Server Action below — NO AUTH CHECK INSIDE

  async function updateProfile(formData: FormData) {
    'use server';
    // ❌ Attacker can POST directly here
    await db.user.update({ where: { id: formData.get('userId') }, data: ... });
  }

  return <form action={updateProfile}>...</form>;
}

// ✅ Correct: Auth inside Server Action
export default async function DashboardPage() {
  async function updateProfile(formData: FormData) {
    'use server';
    const session = await getSession();
    if (!session?.user) {
      throw new Error('Unauthorized');
    }
    // Verify resource ownership
    if (session.user.id !== formData.get('userId')) {
      throw new Error('Forbidden');
    }
    await db.user.update({
      where: { id: session.user.id },
      data: { name: formData.get('name') },
    });
  }

  return <form action={updateProfile}>...</form>;
}

2.2 Preventing IDOR (Insecure Direct Object Reference)

IDOR attacks are the most common vulnerability in Server Actions. A user modifies a postId in the form data to access or modify another user's resource.

// Always check ownership
async function deletePost(formData: FormData) {
  'use server';
  const session = await getSession();
  const postId = formData.get('postId');
  const post = await db.post.findUnique({ where: { id: postId } });
  if (!post || post.authorId !== session?.user?.id) {
    throw new Error('Forbidden');
  }
  await db.post.delete({ where: { id: postId } });
}

2.3 Input Validation and Return Value Control

Never trust searchParams, form data, or URL parameters without schema validation. Use Zod or Yup inside Server Actions, not in the form's client code.

import { z } from 'zod';

const profileSchema = z.object({
  name: z.string().min(2).max(50),
  bio: z.string().max(500).optional(),
});

async function updateProfile(formData: FormData) {
  'use server';
  const session = await getSession();
  if (!session?.user) throw new Error('Unauthorized');

  const parsed = profileSchema.safeParse({
    name: formData.get('name'),
    bio: formData.get('bio'),
  });
  if (!parsed.success) {
    return { errors: parsed.error.flatten().fieldErrors };
  }

  // ✅ Return minimal DTO — never return raw DB record
  await db.user.update({
    where: { id: session.user.id },
    data: parsed.data,
  });
  return { success: true };
}

2.4 Closure Encryption

Server Actions defined inside components capture variables from the surrounding closure. Next.js encrypts these closed-over values automatically. The encryption key is generated per build and can be overridden via NEXT_SERVER_ACTIONS_ENCRYPTION_KEY for self-hosted deployments that need to persist across restarts.

Warning: Don't rely on encryption alone for sensitive values. An attacker who compromises the encryption key (or the build process) can decrypt closed-over values. Use encryption as a defense-in-depth layer, not as the sole protection.

2.5 Allowed Origins for Reverse Proxy Setups

Server Actions compare the Origin header against Host (or X-Forwarded-Host) to prevent cross-origin CSRF attacks. If your deployment uses a multi-layered reverse proxy, configure serverActions.allowedOrigins:

// next.config.js
const nextConfig = {
  serverActions: {
    allowedOrigins: ['my-proxy.com', 'app.internal'],
  },
};

Section 3: Content Security Policy (CSP)

CSP is your strongest defense against XSS and code injection. Next.js 16.x supports three CSP strategies, each with different trade-offs.

3.1 Nonce-Based CSP (Strict Mode)

Nonce-based CSP is the most secure option but has a critical limitation: it requires dynamic rendering because a unique nonce must be generated per request. This means no CDN caching, no ISR, and no PPR. Every page is server-rendered on demand.

// proxy.ts (formerly middleware.ts)
import { NextResponse } from 'next/server';

export function middleware(request) {
  const nonce = crypto.randomUUID();
  const csp = [
    `default-src 'self'`,
    `script-src 'nonce-${nonce}' 'strict-dynamic'`,
    `style-src 'nonce-${nonce}' 'unsafe-inline'`,
    `img-src 'self' data: https:`,
    `base-uri 'none'`,
  ].join('; ');

  const response = NextResponse.next();
  response.headers.set('Content-Security-Policy', csp);
  response.headers.set('x-nonce', nonce);
  return response;
}

export const config = {
  matcher: ['/((?!api/|_next/static|_next/image|favicon).*)'],
};
// App layout — force dynamic rendering
import { connection } from 'next/server';

export default async function RootLayout({ children }) {
  await connection(); // Forces dynamic rendering
  return <html><body>{children}</body></html>;
}

3.2 Hash-Based CSP with SRI (Static Generation)

Use SRI (Subresource Integrity) when you need static generation with CDN caching. Next.js generates integrity hashes at build time and includes them in the CSP header.

// next.config.js
const nextConfig = {
  experimental: {
    sri: {
      algorithm: 'sha384',
    },
  },
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: [
          {
            key: 'Content-Security-Policy',
            value: "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'",
          },
        ],
      },
    ];
  },
};

Limitations: SRI is experimental, App Router only, and cannot handle dynamically injected scripts.

3.3 Static CSP (Simplest Configuration)

For applications with low XSS risk or those that don't require strict CSP, set headers in next.config.js. This allows 'unsafe-inline' for scripts and styles — simpler but significantly less secure.

3.4 Development vs Production CSP

In development mode, React uses eval() for stack traces and hot reloading. Your development CSP must include 'unsafe-eval' in the script-src directive. Use a conditional pattern:

const isDev = process.env.NODE_ENV === 'development';
const csp = [
  `default-src 'self'`,
  `script-src 'nonce-${nonce}' 'strict-dynamic' ${isDev ? "'unsafe-eval'" : ''}`,
  ...
];

Section 4: Dependency Auditing & Supply Chain Security

The Next.js ecosystem, like all npm ecosystems, is vulnerable to supply chain attacks. Malicious packages, typosquatting, and compromised maintainer accounts are real threats. Here's your auditing toolchain and checklist.

4.1 Automated Dependency Scanning

Integrate these tools into your CI/CD pipeline:

4.2 Next.js-Specific Supply Chain Risks

4.3 Next.js Security Audit Checklist

Section 5: Deployment Security

Security doesn't stop at the application layer. Your deployment architecture — headers, reverse proxy, image configuration, and environment variable management — is equally important.

5.1 Security Headers

Configure these headers in next.config.js:

// next.config.js
const nextConfig = {
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: [
          { key: 'X-Content-Type-Options', value: 'nosniff' },
          { key: 'X-Frame-Options', value: 'DENY' },
          { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
          { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
          { key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' },
        ],
      },
    ];
  },
};

5.2 Reverse Proxy: Never Expose Next.js Directly

For self-hosted deployments, a reverse proxy (nginx, Caddy, Traefik) is not optional. It protects against malformed requests, slow connection attacks (Slowloris), and handles TLS termination with proper certificate management.

# Example nginx reverse proxy configuration
server {
    listen 443 ssl http2;
    server_name myapp.com;

    # Rate limiting
    limit_req_zone $binary_remote_addr zone=api:10m rate=30r/s;
    limit_req zone=api burst=20 nodelay;

    # Payload size limits
    client_max_body_size 10m;

    # Proxy to Next.js
    location / {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;

        # Timeouts to prevent slow attacks
        proxy_read_timeout 30s;
        proxy_send_timeout 30s;
    }
}

5.3 Image Optimization Security

Next.js 16.x introduced a breaking change: local IP image optimization is blocked by default. An attacker can no longer exploit image optimization to make your server request internal resources (SSRF via image optimization). If you need to optimize images from a local network, explicitly enable it:

// Only for private networks — not for public deployments
const nextConfig = {
  images: {
    dangerouslyAllowLocalIP: true, // Use only for private/CDN networks
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'cdn.myapp.com',
      },
    ],
  },
};

5.4 Environment Variable Security

Section 6: Architecture & Defense-in-Depth

6.1 Data Access Layer (DAL) Pattern

The DAL pattern centralizes all database access behind authorization-gated, server-only modules that return minimal DTOs. This is the single most effective architecture for preventing data leakage in Next.js.

// lib/data-access.ts
import 'server-only';
import { cache } from 'react';
import { experimental_taintObjectReference } from 'react';

export const getProfileDTO = cache(async (userId: string) => {
  const session = await getSession();
  if (!session?.user) throw new Error('Unauthorized');

  const profile = await db.profile.findUnique({ where: { id: userId } });
  if (!profile || profile.userId !== session.user.id) {
    throw new Error('Forbidden');
  }

  // Return minimal DTO — never the raw DB record
  const dto = {
    displayName: profile.name,
    avatar: profile.avatarUrl,
    bio: profile.bio,
  };

  experimental_taintObjectReference(
    'Do not pass profile record to Client Components',
    profile
  );

  return dto;
});

The server-only package at the top ensures that importing this module from a Client Component causes a build error. The React.cache() wrapper deduplicates repeated calls within the same request — important when the same profile data is needed by multiple Server Components on the same page.

6.2 Zero Trust Model for RSC

Treat every Server Component data access as untrusted by default. Don't assume that because code runs on your server, it's safe. Verify authentication, authorization, and input validation at every boundary:

6.3 Layered CSRF Protection

A single CSRF defense is not enough. Use all layers:

  1. SameSite cookies — set SameSite=Lax or Strict on all cookies (default in modern browsers).
  2. Origin/Host header verification — built into Next.js Server Actions. The framework compares Origin against Host by default.
  3. serverActions.allowedOrigins — explicit origin allowlist for reverse proxy architectures where the Host header may differ from the external origin.
  4. POST-only enforcement — Server Actions reject GET requests by design. Never create a Server Action that accepts GET.

6.4 Avoiding Side Effects During Rendering

Server Components should never trigger side effects during render. Mutations (logout, DB writes, cache invalidation, cookie clearing) belong in Server Actions, not in component bodies. Next.js explicitly prevents cookies and cache revalidation inside render methods. Use Server Actions tied to form submissions for all mutations.

For a broader perspective on full-stack development security and framework comparisons, see my comparison of React vs Next.js, and the React vs Vue vs Angular guide for framework-level security considerations.

Need help securing your Next.js application? Check my Next.js development services.

FAQ

What is CVE-2025-66478 and does it affect my Next.js application?
CVE-2025-66478 is a critical (CVSS 10.0) remote code execution vulnerability in the React Server Components protocol. It affects Next.js 15.x and 16.x App Router applications using the default runtime. Versions 13.x and 14.x stable are not affected. If you are on any 15.x or 16.x version below the fixed patch, you must update immediately: [email protected], 15.1.11, 15.2.8, 15.3.8, 15.4.10, 15.5.9, or 16.0.7+. After updating, rotate all secrets if your app was unpatched on or before December 4, 2025.
Do I need to check authentication inside every Server Action?
Yes, absolutely. Server Actions are reachable via direct POST requests regardless of the UI that invokes them. A page-level authentication check does NOT protect Server Actions defined on that page. Always re-authenticate and re-authorize the user inside each Server Action. This is the single most important security rule for Server Actions — forgetting it is the most common vulnerability in production Next.js applications. To learn more about how React development patterns intersect with secure architecture, see my React vs Next.js comparison.
Can I use CSP with static Next.js pages?
Yes, but not with nonces. Nonce-based CSP requires dynamic rendering because a unique nonce must be generated per request — it is incompatible with CDN caching, ISR, or static export. For statically generated pages, use the hash-based SRI approach (experimental in Next.js 16.x) which generates integrity hashes at build time, or use a simpler strict CSP with script-src 'self' that does not require nonces. The SRI approach supports full static generation and CDN caching.
How should I protect environment variables in Next.js?
Server-side environment variables (no NEXT_PUBLIC_ prefix) are never exposed to the client bundle — they only exist on the server. For production, avoid build-time inlining of secrets. Use runtime environment variables, especially in Dockerized deployments. Never commit .env files to version control — add them to .gitignore. For self-hosted deployments, inject secrets at the container level or use a secrets manager. Rotate secrets immediately if you were running an unpatched Next.js version during the CVE-2025-66478 window.
What is the Data Access Layer (DAL) pattern in Next.js?
The Data Access Layer (DAL) is a security pattern where all database access is centralized behind server-only modules that enforce authentication and authorization checks before returning data. DAL functions use the 'server-only' package to prevent accidental client import and return minimal DTOs instead of raw database records. They can share in-memory caches via React.cache() for repeated reads during a single request. This architecture prevents data leakage through Server Component props to Client Components and is the recommended pattern in the Next.js data security documentation.
Should I expose my Next.js server directly to the internet?
No. For self-hosted deployments, always place a reverse proxy (nginx, Caddy, or Traefik) in front of your Next.js server. The reverse proxy handles TLS termination, rate limiting, payload size limits, and filters malformed requests before they reach your application. This is explicitly recommended by the Next.js self-hosting documentation and is a critical part of any production deployment architecture. It protects against Slowloris attacks, malformed HTTP requests, and resource exhaustion.
How does Next.js protect against CSRF in Server Actions?
Next.js has built-in CSRF protection for Server Actions. They enforce POST-only requests (GET requests are rejected). They also compare the Origin header against the Host (or X-Forwarded-Host) header on every request, blocking cross-origin submissions. For multi-layered backends behind reverse proxies, you can configure serverActions.allowedOrigins in next.config.js to explicitly list allowed proxy origins. For defense in depth, combine SameSite cookies with these measures. This layered approach is the recommended strategy in the Next.js data security guide.

Ready to Secure Your Next.js Application?

Next.js security is a moving target. The RSC protocol vulnerabilities of December 2025 reminded everyone in the ecosystem that new architectural paradigms bring new attack surfaces. The techniques covered here — DAL pattern, Server Action auth, CSP configuration, dependency auditing, and reverse proxy deployment — form a practical defense-in-depth strategy for any production Next.js application.

I've been building full-stack applications for over 20 years, including multiple production Next.js deployments. If you need help auditing your application's security, implementing the DAL pattern, or configuring a hardened deployment, reach out for a free consultation.

I'm a full-stack developer based in Minsk, working with clients worldwide. Let's discuss your project.

Contact

Let's discuss your Next.js security

Need help auditing or securing your Next.js application? I provide free initial consultations.