4 min read

Server Action auth guard — every Server Action needs session check

Server Actions execute server-side but default to no auth. Add session-required guard at the top of every protected action.

Server Action = server-side function callable from client. Without auth check at the top, any caller can invoke it.

What it is

Server Actions are Next.js's mechanism for client-to-server RPC. Just because they run server-side doesn't mean they're authenticated by default.

Vulnerable example

"use server";
export async function transferFunds(toUserId: string, amount: number) {
  // No session check — any caller (logged-in or not) can transfer funds
  await db.transfer({ from: "???", to: toUserId, amount });
}

Fixed example

"use server";
import { auth } from "@/auth";
export async function transferFunds(toUserId: string, amount: number) {
  const session = await auth();
  if (!session) throw new Error("unauthenticated");
  // Plus authorization: confirm the from-account belongs to this session
  const fromAccount = await db.accounts.findOne({ userId: session.user.id });
  if (!fromAccount) throw new Error("forbidden");
  await db.transfer({ from: fromAccount.id, to: toUserId, amount });
}

How Securie catches it

Securie findinghigh
apps/web/auth.ts:87

Server Action auth guard

AuthAuthz specialist catches Server Actions without session check at PR time. Sandbox-verifies by attempting the action without a session.

Suggested fix — ready as a PR
"use server";
import { auth } from "@/auth";
export async function transferFunds(toUserId: string, amount: number) {
  const session = await auth();
  if (!session) throw new Error("unauthenticated");
  // Plus authorization: confirm the from-account belongs to this session
  const fromAccount = await db.accounts.findOne({ userId: session.user.id });
  if (!fromAccount) throw new Error("forbidden");
  await db.transfer({ from: fromAccount.id, to: toUserId, amount });
}
Catch this in my repo →Securie scans every PR · ships the fix as a one-click merge · free during early access

Checklist

  • const session = await auth() at top of every action
  • if (!session) throw — fail-closed
  • Authorization check after authentication
  • Re-MFA for high-risk actions
  • Audit log every state change

FAQ

What about read-only actions?

Same pattern — even reads need auth if returning user-specific data.

Related guides