How to Secure React Server Components
React Server Components (RSC) shift rendering to the server, improving performance but introducing new security considerations. This article explains how to secure your React Server Components by preventing data leaks, restricting server-only code to the server, validating user inputs, and implementing secure authentication and authorization practices.
Keep Server Code
Private with server-only
Because React allows you to write client and server code in the same project, it is easy to accidentally import server-side module code into a Client Component. If this happens, sensitive server-side logic, environment variables, or private API keys can leak into the client-side JavaScript bundle.
To prevent this, use the server-only package. Install it
via npm:
npm install server-onlyThen, import it at the top of any file that should run exclusively on the server (e.g., database connection helpers, API clients):
import 'server-only';
export async function getSensitiveData() {
// Database queries or API calls with private keys
}If a developer accidentally imports this file into a Client Component, the build process will fail, preventing the leak.
Prevent Data Leaks via Prop Serialization
When a Server Component passes data to a Client Component as a prop, React serializes that data to send it across the network. If you pass an entire database record, you may accidentally send sensitive fields (like hashed passwords, internal IDs, or user emails) that the client does not need.
To secure your data: * Create Data Transfer Objects
(DTOs): Manually extract only the properties required by the
Client Component. * Avoid spreading objects: Do not use
{...userData} when passing props to Client Components. *
Validate data structures: Use validation libraries to
strip out unapproved fields before the data leaves the server.
// Secure approach: Only pass what is necessary
const userData = await db.user.findUnique({ where: { id } });
const safeUser = {
name: userData.name,
avatar: userData.avatar
};
return <UserProfile user={safeUser} />;Validate Inputs in Server Actions
Server Actions allow Client Components to invoke server-side functions directly. Under the hood, these actions are exposed as HTTP POST endpoints, meaning anyone can call them with any payload.
You must treat Server Actions exactly like public API endpoints: * Validate all incoming payloads: Use a schema validation library like Zod to ensure the data format is correct. * Sanitize inputs: Prevent injection attacks by escaping and validating string inputs.
'use server';
import { z } from 'zod';
const schema = z.object({
email: z.string().email(),
message: z.string().min(1).max(500),
});
export async function submitContactForm(formData) {
const result = schema.safeParse(formData);
if (!result.success) {
throw new Error('Invalid input');
}
// Proceed with safe, validated data
const { email, message } = result.data;
}Enforce Authentication and Authorization
React Server Components have direct access to your backend infrastructure, making it easy to fetch data straight from your database. However, you must explicitly check permissions before fetching data.
- Authenticate on the server: Read cookies or session tokens directly within your Server Components or data-access layer to identify the current user.
- Authorize data access: Do not rely on UI-level
hiding (e.g.,
{isAdmin && <AdminPanel />}). Verify that the authenticated user has the correct permissions to view the data before fetching it.
import { getSession } from './auth';
export default async function AdminDashboard() {
const session = await getSession();
if (!session || session.user.role !== 'admin') {
throw new Error('Unauthorized access');
}
const adminData = await fetchAdminData();
return <main>{/* Render admin dashboard */}</main>;
}Control Server Action Execution Permissions
Just as you authorize Server Components, you must authorize Server Actions. Never assume that because a button is hidden in the client UI, a user cannot trigger the corresponding Server Action.
Always verify the user’s session and permissions at the very beginning of the Server Action function:
'use server';
import { getSession } from './auth';
export async function deletePost(postId) {
const session = await getSession();
if (!session) {
throw new Error('Authentication required');
}
const post = await db.post.findUnique({ where: { id: postId } });
if (post.authorId !== session.user.id) {
throw new Error('You do not have permission to delete this post');
}
await db.post.delete({ where: { id: postId } });
}