SuperAPI AI Directory

nextjs-server-actions-security

Frontend & websites

View repository

Updated 10-15 20:31

About this skill

Next.js Server Actions Security

Every Server Action is a public POST endpoint. The bundler hides the name; it does not hide the contract. Who can call the action is the network — the action must fail closed.

Implementation of actions: nextjs-server-actions. General authn/authz: authentication-authorization.

Threat model

Threat Why
Enumerable endpoint Action IDs leak in the client bundle; POST can be forged
IDOR id in FormData/bind comes from the attacker
CSRF / cross-origin Authenticated browser + cross-site POST
Mass assignment Spread of FormData into the update
Data leak Return/throw with internal fields
Abuse No rate limit on login, payment, invite

Non-negotiable rules

  1. Authn + authz in the body of the action — middleware/layout is not enough (they can be bypassed).
  2. Fail closed — no session or no policy → { ok: false, code: "unauthorized"|"forbidden" } or forbidden().
  3. Never trust IDs/fields from the client — reload the resource and check ownership/tenant.
  4. Allowlist parsing — Zod schema (or equivalent); ignore extra fields.
  5. Minimal response — no hashes, tokens, PII, stack, SQL.

Authz inside the action

"use server";

export async function deletePost(formData: FormData) {
  const user = await requireUser(); // cookies/session no server
  const id = z.string().uuid().parse(formData.get("id"));

  const post = await db.post.findUnique({ where: { id } });
  if (!post || post.tenantId !== user.tenantId) {
    return { ok: false as const, code: "not_found" }; // não vaze existência cross-tenant
  }
  if (post.authorId !== user.id && user.role !== "admin") {
    return { ok: false as const, code: "forbidden" };
  }

  await db.post.delete({ where: { id } });
  revalidatePath("/posts");
  return { ok: true as const };
}
  • requireUser() in every authenticated mutation; public actions (e.g. newsletter) are an explicit exception + rate limit.
  • Distinguish 401 (anonymous) from 403 (authenticated without permission) internally; for multi-tenant IDOR prefer a uniform 404.
  • Roles in JWT: revalidate critical permissions on the server (session/DB), not just the old claim.

CSRF, origin, and proxies

  • Next.js checks Origin/Host (and equivalent headers) in Server Actions in recent versions — do not disable.
  • Behind a reverse proxy: configure serverActions.allowedOrigins (and experimental.serverActions if the project still uses the old key) with the real hosts.
  • Session cookies: HttpOnly, Secure, SameSite=Lax (or Strict if the flow allows it). SameSite=None only with a reason.
  • Do not mark actions as “CSRF-safe” just because they use POST; Origin + SameSite + authz together.
// next.config.ts — só os hosts do produto
experimental: {
  serverActions: {
    allowedOrigins: ["app.example.com", "www.example.com"],
  },
},

Adjust to the config key of the repo's Next major version.

Hostile input

  • FormData, JSON, and arguments of .bind are controlled by the client (the runtime encrypts closures; that is not authz).
  • Forbidden: db.update({ data: Object.fromEntries(formData) }).
  • Uploads: mime/size on the server; do not trust type from the browser; store outside the webroot with opaque names.
  • Path/URL strings: allowlist; block javascript:, //evil, open redirects.

What not to close / not to return

  • Do not capture secrets, connection strings, or tokens in closures sent to the client.
  • Do not pass from the server to the action via bind: huge objects, the complete user, admin flags "already checked".
  • Explicit return/select. Errors: code stable + message safe; log digest internally.

Rate limit and abuse

  • Limit per action + identity (user id or IP) on login, signup, invite, payment, mail.
  • Idempotency in financial mutations / jobs (Idempotency-Key or business key).
  • Reject large payloads (serverActions.bodySizeLimit aligned with the actual upload).

Checklist per new action

  • Session resolved on the server
  • Policy/ownership on the resource (not just "is logged in")
  • Schema allowlist; IDs revalidated
  • No mass assignment
  • Return without internal fields
  • Revalidate only what the user can see
  • Rate limit if it is expensive or authenticable by anonymous users
  • allowedOrigins correct in the deploy (www, preview, proxy)

Anti-patterns

  • Trusting a hidden button / disabled in the UI
  • Auth only in middleware.ts
  • Treating action ID as a secret
  • export async function generic that accepts op + payload (open RPC)
  • dangerouslyAllowSVG / user HTML without sanitizing in rich fields
  • Disabling origin verification because "the proxy broke"

Completion criteria

  • Every action touched authenticates and authorizes on the server
  • IDs/tenant without demonstrable IDOR
  • Schema allowlist; no spread of FormData into the DB
  • Origin/cookies/limits aligned with the deploy
  • Errors and returns without leakage
  • Abuse paths (auth, mail, payment) with limit or idempotency