---
title: "Server Actions with no auth check"
slug: server-actions-without-auth
severity: critical
category: Access control
summary: "A Server Action is a public POST. 'use server' does not mean only your button can call it."
platforms:
  - Next.js
  - Supabase
  - Vercel
updated: "2026-10-13"
related:
  - auth-checked-only-in-the-ui
  - idor
  - client-sets-price-or-role
---

Next.js Server Actions let a component call a server function without you writing a route. The framework still exposes that function as an HTTP endpoint. Anyone who can talk to your site can invoke it. The button is one client. It is not the only client.

`"use server"` marks the function as server code. It does not check who the caller is. An action that updates a row using an id from its arguments, and never looks at the session, is an open write. Combined with the secret key, it also bypasses Supabase RLS.

Closures do not save you. The arguments the client passes are untrusted. A hidden input with `userId` is untrusted. Read the user from the session inside the action.

## How to check in 5 minutes

1. Search for `"use server"`. Open every exported async function.
2. For each, find the session lookup. If the first `await` is a database call, the action is open.
3. Check arguments. If the function trusts `userId`, `role`, `price`, or `isAdmin` from the caller, that is the bug even when a session exists.
4. Sign out and trigger the action's endpoint if you know it, or temporarily call the action from a logged-out page on a staging deploy you own. A mutation that succeeds logged out is confirmed.

## How to fix

### Next.js

```ts
"use server";

export async function renameProject(projectId: string, name: string) {
  const user = await getCurrentUser();
  if (!user) throw new Error("Unauthorized");

  const project = await db.project.findFirst({
    where: { id: projectId, userId: user.id },
  });
  if (!project) throw new Error("Not found");

  await db.project.update({ where: { id: project.id }, data: { name } });
}
```

Validate `name` for length and type. Do not spread the form object into `data`. That is how `role` sneaks in. See [the client sets the price or role](/issues/client-sets-price-or-role).

Middleware or proxy can be an extra filter. Keep the check in the action anyway.

### Supabase

Inside the action, create the client from the user's cookies so RLS runs, or use the secret key only after the session check and add `user_id` to the query yourself. Do not create a module-level secret client and call it from every action by habit.

### Vercel

Server Actions work on Vercel the same way. Deployment protection on a preview does not authorize production actions.

### Firebase

If the action uses the Admin SDK, rules will not stop it. The session check in the action is the control.

## Agent prompt

```text
You are authorizing Server Actions. Do not add features.

Find every "use server" function that reads or writes data.

1. Load the current user inside the function. If there is no user, throw or return an error before any write.
2. Ignore userId, role, price, and isAdmin arguments. Take identity from the session.
3. Scope updates and deletes to rows that user owns.
4. Do not pass the raw form object into a database update. Assign the fields you intend.
5. Leave the UI button as it is. The button is not the check.

List each action and the check you added.
```

## FAQ

### Are Server Actions CSRF-safe because Next.js checks the origin?

Next.js has protections so a random site cannot trivially submit an action with the user's cookies. That is not authorization. A request that passes the framework's origin check can still be from a logged-out caller or from the wrong user. Check the session and the owner.

### Can I rely on the layout having redirected?

No. The action does not re-run your layout's redirect as its authorization. Check inside the function.

## Sources

- [OWASP Authorization Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authorization_Cheat_Sheet.html)
- [Supabase API keys](https://supabase.com/docs/guides/api/api-keys)
