Lesson 15 / 25

Securing Server Actions

Every action is a public endpoint.

Authenticate, authorise, validate

A Server Action can be called with any arguments by anyone who can reach your site, just like an API endpoint. Inside every action: authenticate the user (read the session), authorise the specific operation (may this user edit this record?), and validate all input with a schema (for example Zod) rather than trusting form fields. Return safe error messages, avoid leaking internal details, and rate-limit sensitive actions.

A guarded action (sketch)

Pattern for real actions; getSession and db are your own modules. Not run here.

"use server";
import { z } from "zod";
import { getSession } from "@/lib/auth";

const Input = z.object({ id: z.string().uuid(), price: z.number().positive().max(1_000_000) });

export async function setPrice(raw: unknown) {
  const session = await getSession();
  if (!session) return { error: "Please sign in" };                 // authenticate
  const input = Input.safeParse(raw);
  if (!input.success) return { error: "Invalid input" };           // validate
  const product = await db.product.find(input.data.id);
  if (product.ownerId !== session.userId) return { error: "Not allowed" };   // authorise
  await db.product.update(input.data.id, { price: input.data.price });
  return { ok: true };
}

Never trust hidden fields

Users can change any value sent from the browser, including hidden inputs and ids; check permissions on the server.

Quick check: Why must Server Actions check authorisation?

  • They can be called directly with any arguments, like public endpoints
  • React checks permissions automatically
  • They only run in development
  • Forms cannot be submitted by attackers
Answer

They can be called directly with any arguments, like public endpoints — Treat actions as public APIs.