'use server' Doesn't Mean Private
Frontend Security Essentials — Part 1
'use server' Doesn't Mean Private
Frontend Security Essentials — Part 1
The Hook
Imagine a deleteUser Server Action shipped to prod with no auth check inside — the dev assumed "use server" made it private. It doesn't. A curl one-liner, lifted from the browser's network tab, can wipe accounts without ever loading your UI.
The Core Idea
Server Actions in Next.js and Nuxt feel like ordinary functions you import. They’re not. Every "use server" function compiles into a POST endpoint with a hashed URL, and that hash ships inside your client bundle — fully discoverable by anyone with DevTools within thirty seconds. The syntax tells you nothing about the boundary you just crossed.
The mental model that saves you: a Server Action is a public API route wearing a function signature. Anyone on the internet can invoke it. Your UI is one possible caller, not the only one.
Next.js 14+ does handle CSRF for you via origin checks and SameSite cookies. What it does not handle: authentication, authorization, or input validation. That stays on you — and the absence of a route file is exactly why it gets skipped in review.
The Mistake
// app/actions.ts
'use server'
// ❌ Vulnerable - looks internal, callable by anyone
export async function deleteUser(userId: string) {
await db.users.delete(userId)
}
The admin button is hidden behind a role check in the UI. The endpoint is wide open. A logged-out attacker POSTs to the action URL with any userId and the row is gone.
The Fix
'use server'
import { getServerSession } from '@/lib/auth'
import { z } from 'zod'
const Input = z.object({ userId: z.string().uuid() })
// ✅ Secure - authn + authz + validation inside the action
export async function deleteUser(raw: unknown) {
const session = await getServerSession()
if (session?.user?.role !== 'admin') throw new Error('Unauthorized')
const { userId } = Input.parse(raw)
await db.users.delete(userId)
}
Session, role, schema — in that order, every time. Hide the button too if you want, but never as the only gate.
One Rule To Remember
Hiding a button is not access control. Every Server Action enforces its own auth, or it has none.
Quick Win
- Grep your codebase for
'use server'. Confirm every action starts with a session and role check before it touches the database. - Wrap every action’s input with a Zod or Valibot schema. Untyped
unknownin, typed object out — no exceptions. - Write one failing test per action: an unauthenticated client invoking it should be rejected. If it succeeds, you just found your bug.
메타데이터
- post_id
- fbffbca20ea3
- slug
- use-server-doesn-t-mean-private-fbffbca20ea3
- url
- https://medium.com/@raselhasan11/use-server-doesn-t-mean-private-fbffbca20ea3
- canonical_url
- https://medium.com/@raselhasan11/use-server-doesn-t-mean-private-fbffbca20ea3
- author_url
- https://medium.com/@raselhasan11
- status
- ok
- fetched_at
- 2026-06-09 15:37:30