The Biggest Mistake You’re Making with Next.js Server Actions: Silent Threat
One of the critical mistakes I learned in my Next.js journey is failing to validate and protect server actions. Server actions in Next.js can expose post routes, which can be vulnerable if not properly secured. When using server actions, it's essential to safeguard both the integrity of the data being sent and the security of the route itself. This article will guide you through how to implement validation and authentication checks in your server actions.
Understanding the Exposure of Server Actions
In Next.js, when you submit a form that triggers a server action, you might notice that a POST request is made to the same route where the action is used. For example, if you have a form on the route localhost:3000/products
, submitting the form triggers a POST request to this URL, exposing that route via the Network tab in your browser's developer tools.
Now, while this might seem harmless when done through your user interface (UI), a malicious actor could gain access to this route and send arbitrary data to your server by hitting the exposed endpoint directly. If left unprotected, it can result in serious issues like unauthorized data manipulation or injection attacks.
Example Scenario
Let’s say we have a form to add a new product using a server action. The form submits data to localhost:3000/products
.
// app/actions/productActions.tsx
"use server";
import { prisma } from "@/lib/prisma";
export async function addProductAction(productData: { name: string, price: number }) {
await prisma.product.create({
data: productData,
});
}
When this action is triggered by a form submission, Next.js automatically generates a POST
request to localhost:3000/products
. However, if someone manually sends an unauthenticated or incorrect request to this route, it could cause problems.
Validating Incoming Data with Zod
To protect your server action from receiving invalid or harmful data, you should always validate the data using a library like Zod. Zod is a powerful schema-based validation library that allows you to define the shape and structure of the data your server action expects.
Adding Zod Validation to Server Action
Here’s how you can integrate Zod to ensure that only properly formatted data reaches your server action:
// app/actions/productActions.tsx
"use server";
import { prisma } from "@/lib/prisma";
import { z } from "zod";
// Define a schema for validating the product data
const productSchema = z.object({
name: z.string().min(1, "Product name is required"),
price: z.number().positive("Price must be a positive number"),
});
export async function addProductAction(productData: { name: string, price: number }) {
// Validate the incoming product data
const validatedData = productSchema.parse(productData);
await prisma.product.create({
data: validatedData,
});
}
In this example:
We define a Zod schema that expects a
name
(string) and aprice
(positive number).Before performing any database operations, we validate the incoming
productData
usingproductSchema.parse
. If the data is invalid, Zod will throw an error and prevent further execution.
This way, we ensure that only correctly formatted data is processed by the server action.
Securing the Action with Authentication
Another critical layer of protection is ensuring that only authenticated users can trigger certain server actions. If your server action is exposed, you don’t want unauthorized users to access sensitive operations like adding or modifying data.
Let’s assume that only logged-in users can add new products. You can enforce authentication checks in your server action using NextAuth.js or any other authentication library.
Adding Authentication Check
Here’s how you can add authentication to your server action using NextAuth.js:
// app/actions/productActions.tsx
"use server";
import { prisma } from "@/lib/prisma";
import { z } from "zod";
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/lib/auth";
// Define the product data schema
const productSchema = z.object({
name: z.string().min(1),
price: z.number().positive(),
});
export async function addProductAction(productData: { name: string, price: number }) {
// Validate the product data
const validatedData = productSchema.parse(productData);
// Authenticate the user
const session = await getServerSession(authOptions);
if (!session) {
throw new Error("Unauthorized: You must be logged in to add a product.");
}
// Proceed with adding the product
await prisma.product.create({
data: validatedData,
});
}
In this example:
We use
getServerSession
from NextAuth.js to check if the user is logged in.If no session is found, an error is thrown, preventing unauthorized access to the action.
If the user is authenticated, the server action proceeds with the data validation and database update.
Conclusion: Always Validate and Secure Your Server Actions
Server actions in Next.js offer great flexibility, allowing you to interact with your backend effortlessly. However, security and data validation should always be at the forefront when building server-side functionality.
Key takeaways:
Validate incoming data: Use libraries like Zod to ensure that only valid data gets processed by your server action.
Authenticate sensitive actions: Make sure to authenticate users before performing sensitive operations like database updates.
By incorporating these practices, you can ensure your Next.js app remains secure, even when handling client-server interactions through server actions.
Subscribe to my newsletter
Read articles from Rishi Bakshi directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Rishi Bakshi
Rishi Bakshi
Full Stack Developer with experience in building end-to-end encrypted chat services. Currently dedicated in improving my DSA skills to become a better problem solver and deliver more efficient, scalable solutions.