Skip to content
Agentmart
Submit a tool

nextjs-server-action-safety

Defend against client/server boundary confusion in Next.js Server Actions.

Skill by MILTONADINA

IndexedMIT
Write a review

Install

Works with Claude Code, Claude apps, Codex, Cursor, Gemini CLI, GitHub Copilot, VS Code, OpenCode, Goose, Windsurf. Read the repository before you give any tool access to your data.

npx agentmarket install skill/miltonadina-nextjs-server-action-safety

Asks which of your tools to add it to (Claude Code, Codex, Cursor, VS Code, Windsurf, Gemini CLI, Claude Desktop) and shows the change first. See https://agentmart.dev/cli.

Get the weekly diff

The weekly diff: one email a week with new agent tools, notable updates and grade changes, plus security alerts for tools you follow. Double opt-in, one-click unsubscribe.

We use your email only for this newsletter. Privacy

About

Defend against client/server boundary confusion in Next.js Server Actions. Patterns covered include authn drift in token-bound mutations, input validation at the action boundary, and mutation idempotency under retry. Triggers on Server Action authoring (functions with `use server` directive) and Next.js form-action references. OWASP ASI02 (Tool Misuse) plus ASI03 (Identity & Privilege Abuse -- authn drift) defense.

Use cases: frontend, backend, security

No reviews yet. Installed it? Reviews come from verified installers only. How reviews work

Permissions

What this skill can do and what it asks for, from its SKILL.md. Checked Oct 25, 2026.

Tools
No tools found
Secrets
None asked for
Install scripts
None found

Network hosts it talks to 1

  • codeload.github.com · found in install command

Files it touches

  • ~/.claude/skills/nextjs-server-action-safety

What's inside

Skill name
nextjs-server-action-safety
Description
Defend against client/server boundary confusion in Next.js Server Actions. Patterns covered include authn drift in token-bound mutations, input validation at the action boundary, and mutation idempotency under retry. Triggers on Server Action authoring (functions with `use server` directive) and Next.js form-action references. OWASP ASI02 (Tool Misuse) plus ASI03 (Identity & Privilege Abuse -- authn drift) defense.
License
MIT
Instructions
280 lines in SKILL.md · view on GitHub

Agent Skills spec issues

  • name: "nextjs-server-action-safety" does not match folder "server-action-safety"

Files (1)

  • SKILL.md

SKILL.md (first lines)

# Next.js Server Action Safety

> Defends against **OWASP ASI02 (Tool Misuse)** -- a Server Action invoked
> from a wrong client context (logged-out tab, different user's session, an
> attacker's CSRF-shaped payload) re-invokes the server-side mutation with
> the agent's logged-in privileges -- AND **OWASP ASI03 (Identity & Privilege
> Abuse)** -- authn drift where a token-bound mutation runs with stale or
> elevated identity because the action implicitly trusts client-passed
> context instead of re-deriving identity from the server-side session.

**Tradeoff:** Server Actions are a powerful ergonomic for mutations -- they
collapse the form-POST + route-handler + revalidation triad into one
function declaration. The cost: the function is exposed at a public URL
(Next.js compiles each action to a callable endpoint), so it must be
treated like any other public mutation entry point. Without the patterns
below, the convenience leaks into the security model.

---

## Why this matters

Server Actions look like local function calls (`await myAction(formData)`)
but they're actually HTTP POST endpoints generated at build time. Any
authenticated tab can invoke them. Any unauthenticated client can probe
the endpoint URL. Any code in the same Next.js app can import and call
them. The "server-only" annotation (`'use server'`) marks the boundary --
it does NOT add authentication, authorization, validation, or rate
limiting.

A handler that doesn't explicitly re-verify identity, validate input, and
guard against replay is structurally identical to an unauthenticated POST
endpoint. The familiar function-call surface hides this fact from authors
who haven't shipped Next.js mutations in production.

---

## Pattern 1 -- Re-derive identity from server-side session inside every action

Inside the action, ALWAYS read the session from a server-side source
(cookie-backed session store, `auth()` helper, JWT verified against your
own signing secret). Never trust an `actorId` / `userId` / `tenantId`
passed as a form field or function argument -- it's attacker-controllable.

```ts
'use server';

import { auth } from '@/lib/auth';
import { db } from '@/lib/db';
import { z } from 'zod';

const UpdateProfileInput = z.object({
  displayName: z.string().min(1).max(120),
  bio: z.string().max(2000).optional(),
});

export async function updateProfile(formData: FormData) {
  // 1. Re-derive identity. session.userId is the server-side truth.
  const session = await auth();
  if (!session?.userId) {
    throw new Error('unauthenticated');
  }

  // 2. Validate input at the boundary -- the function signature accepts
  //    FormData (any shape); Zod is the runtime contract.
  const parsed = UpdateProfileInput.safeParse({
    displayName: formData.get('displayName'),
    bio: formData.get('bio'),
  });
  if (!parsed.success) {
    throw new Error('invalid input');
  }

  // 3. Scope the mutation to the session-derived identity, not any
  //    client-passed identifier. db.user.update WHERE id = session.userId.
  await db.user.update({
    where: { id: session.userId },
    data: parsed.data,
  });
}
```

A defender reviewing this action can answer in one minute: "Who is this
mutation acting on?" The answer is "the server-derived session user, not
whoever the client says they are."

---

## Pattern 2 -- Validate input with a runtime schema at the action boundary

TypeScript types are compile-time only. The `FormData` argument is the
real runtime contract -- it's whatever the client posted, which may be
arbitrary strings, arrays, or absent fields.

Use Zod / Valibot / a similar runtime validator. Validate at the first
line of the action body. Reject invalid input with a generic error
message (don't echo the invalid value back -- it's potentially attacker-
controlled and may be reflected somewhere).

The validator's schema is also documentation: a defender can read it and
know exactly which fields the action accepts and what the bounds are.

---

## Pattern 3 -- Idempotency for mutations that the client may retry

Server Actions are invoked from forms and from optimistic-UI patterns,
both of which can retry on transient failure (network blip, slow render,
user double-clicks Submit). A mutation that books a flight, charges a
card, or sends a message has to handle the retry without producing two
side effects.

The universal pattern: an idempotency key passed through the action and
checked against a dedupe store. The Stripe-style approach works here too:

```ts
'use server';

export async function chargeOrder(formData: FormData) {
  const session = await auth();

From MILTONADINA/DevOPs, MIT.

More from MILTONADINA/DevOPs

Signals

Stars
0
Forks
0
Open issues
12
Last commit (this folder)
Sep 25, 2026 (1 month ago)
Last release
Sep 23, 2026 (1 month ago)

From GitHub (fetched Oct 24, 2026).

Details

License
MIT
First seen
Oct 24, 2026
Last checked
Oct 24, 2026

More from MILTONADINA

Cite this page

nextjs-server-action-safety skill. Agent Mart. https://agentmart.dev/skills/miltonadina-nextjs-server-action-safety (last updated 2026-10-24)