React Server Components turned your frontend into a server attack surface. Most vulnerabilities now come from Server Actions.
Here are 6 patterns that close the most common holes immediately.
1. Validate Every Server Action Input With Zod
Server Actions receive untrusted input. Treat them like public APIs.
Before
'use server';
export async function createJob(formData: FormData) {
const title = formData.get('title');
const company = formData.get('company');
await db.jobs.create({
data: { title, company }
});
}
After
'use server';
import { z } from 'zod';
const schema = z.object({
title: "z.string().min(3).max(200),"
company: z.string().min(2).max(100),
});
export async function createJob(formData: FormData) {
const parsed = schema.safeParse({
title: "formData.get('title'),"
company: formData.get('company'),
});
if (!parsed.success) {
return { error: parsed.error.flatten() };
}
await db.jobs.create({
data: parsed.data
});
}
You eliminate malformed payloads and injection vectors. This blocks an entire class of exploits with ~10 lines of code.
2. Add Explicit Auth Checks Inside Every Action
"use server" does not enforce authentication. Every action is a public endpoint.
Before
'use server';
export async function deleteJob(id: string) {
await db.jobs.delete({ where: { id } });
}
After
'use server';
import { getSession } from '@/lib/auth';
export async function deleteJob(id: string) {
const session = await getSession();
if (!session?.userId) {
throw new Error('Unauthorized');
}
const job = await db.jobs.findUnique({ where: { id } });
if (job?.authorId !== session.userId) {
throw new Error('Forbidden');
}
await db.jobs.delete({ where: { id } });
}
Without this, anyone can call your endpoint directly with fetch. This is the most common production mistake.
3. Never Leak Full Database Objects From Server Components
Server Components serialize data to the client. If you fetch everything, you expose everything.
Before
export default async function Profile() {
const user = await db.users.findUnique({
where: { id: session.userId }
});
return <ProfileView user={user} />;
}
After
export default async function Profile() {
const user = await db.users.findUnique({
where: { id: session.userId },
select: {
name: true,
avatar: true,
role: true,
}
});
return <ProfileView user={user} />;
}
You reduce the attack surface and prevent accidental exposure of passwords, tokens, and internal flags.
4. Move Secrets Out of Source Code Completely
If source leaks, hardcoded secrets leak with it.
Before
export async function fetchData() {
return fetch('https://api.example.com', {
headers: {
Authorization: 'Bearer sk_live_abc123'
}
});
}
After
export async function fetchData() {
return fetch('https://api.example.com', {
headers: {
Authorization: `Bearer ${process.env.API_SECRET}`
}
});
}
This is basic, but still violated in real projects. One leak equals full compromise.
5. Add CSP Headers at the Proxy Layer
Next.js does not protect you by default. You must define security headers.
Before
export function proxy() {
return NextResponse.next();
}
After
import { NextResponse } from 'next/server';
export function proxy() {
const res = NextResponse.next();
res.headers.set(
'Content-Security-Policy',
"default-src 'self'; script-src 'self'; img-src 'self' data: https:;"
);
res.headers.set('X-Frame-Options', 'DENY');
res.headers.set('X-Content-Type-Options', 'nosniff');
return res;
}
CSP alone blocks many XSS and injection attempts without touching your app code.
6. Detect Suspicious Server Action Requests
Real attacks look like weird payloads hitting your endpoints.
Before
export async function action(formData: FormData) {
// logic
}
After
import { headers } from 'next/headers';
export async function action(formData: FormData) {
const contentType = (await headers()).get('content-type');
if (!contentType?.includes('multipart/form-data')) {
console.error('Suspicious request', {
contentType,
time: new Date().toISOString(),
});
}
// normal logic
}
This gives you early signals of exploitation attempts instead of discovering issues after damage.
This pattern becomes critical as AI-generated code increases attack surface, which is covered deeper in the web security risks introduced by AI generated JavaScript code.
Closing
Pick one action and harden it today. Add validation. Add auth. Add logging.
Security in Next.js is not a config. It is code you write or forget to write.
Top comments (1)
Exactly! I've recently moved to a strict Zod + React Hook Form workflow to ensure every bit of input is sanitized. But as you mentioned, the real danger is the 'Bridge'βthe API. Itβs wild how many devs forget to audit their API output. I'm now using extra layers to ensure the database doesn't leak personal info or sensitive fields in the JSON response. If you neglect API security, you're basically leaving the front door wide open for a total system compromise.