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.
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.
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.
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.
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 |
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.
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.
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 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.
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.
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>;
}
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 } });
}
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 };
}
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.
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'],
},
};
CSP is your strongest defense against XSS and code injection. Next.js 16.x supports three CSP strategies, each with different trade-offs.
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>;
}
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.
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.
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'" : ''}`,
...
];
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.
Integrate these tools into your CI/CD pipeline:
npm audit --audit-level=high in your CI pipeline.package.json scripts and use npm install --ignore-scripts in CI, then verify required scripts against a known-good list.next-js (hyphen) vs next, @vercel/og lookalikes. Use lockfiles and verify package provenance.server-only modules?/[param] values validated and sanitized before use in queries?experimental.taint: true enabled in next.config.js?Security doesn't stop at the application layer. Your deployment architecture — headers, reverse proxy, image configuration, and environment variable management — is equally important.
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' },
],
},
];
},
};
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;
}
}
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',
},
],
},
};
NEXT_PUBLIC_ prefix) are never exposed to the client bundle..env.local and .env.*.local files must be in .gitignore.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.
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:
A single CSRF defense is not enough. Use all layers:
SameSite=Lax or Strict on all cookies (default in modern browsers).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.
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.
Need help auditing or securing your Next.js application? I provide free initial consultations.