Next.js server actions security: every action is a public endpoint
A server action looks like a normal function you call from a button. Under the hood it's a POST endpoint, and anyone who has its ID can call it with any arguments. That's the core of Next.js server actions security: AI coding tools write these actions fast, and it's easy to leave the login check out of the action when the page around the button already has one.
QUICK CHECKLIST
- Check the session inside every action that reads or writes private data
- Take the user id from the session, never from arguments
- Check the record owner before update or delete
- Validate arguments with zod, write named fields only
- Return only what the UI needs
Why the page check doesn't protect the action
The Next.js data security guide says an exported server action "is reachable via a direct POST request, not just through your application's UI." It also says a page-level auth check "does not extend to the Server Actions defined within it."
So a redirect on /admin only hides the admin UI. The deleteUser action behind the button still runs for anyone who sends the request, logged in or not. Next.js gives actions encrypted IDs and drops unused ones from the build, but the docs still tell you to verify auth and authorization inside each action.
Three patterns to look for
No login check. The action writes to the database and never asks who is calling:
'use server'
export async function deleteAccount(userId: string) {
await prisma.user.delete({ where: { id: userId } })
}Logged in, but not the owner. The action checks for a session, then deletes or updates by an id from the client without checking that the row belongs to that user. Any account can delete anyone's posts. This is the IDOR case the Next.js docs warn about.
The whole client object goes into the database. prisma.user.update({ data: formObject }) writes every field the browser sent, including role or credits if the caller adds them.
What we found in public repos
We searched public GitHub code for 'use server' files in an actions folder that call Prisma's .delete(, and read one actions file from each of 48 repos. Our check flagged 25 of them, with 68 actions that write to the database and show no login check. Those numbers come from the automated check. We then read 10 of the flagged repos by hand, and in all 10 the login check was missing from the action. Roughly 10 of the 25 were tutorials or demos with no login at all.
The rest were real apps. Examples: a deleteAccount(userId) that deletes any user, an admin deleteUser(userId) with no admin check, and an action that sets any user's password from an id and a new password. In a second sample of 15 repos that already use session.user.id, we found public actions that hand out a signup bonus to any user id, and organization updates and deletes that work on any tenant's id.
We picked these files because they matched the search, so this is not a rate across all Next.js apps. We didn't contact the repo owners or test anything live.
Fix Next.js server actions security in six steps
Check the session in every action
Do it at the top of every action that writes or reads private data, even if the page already checks it.
Take the user id from the session
Never from the arguments.
deleteAccount()should delete the caller's own account.Check ownership or role
Before an update or delete, check that the record belongs to the caller.
deleteMany({ where: { id: postId, authorId: session.user.id } })checks ownership in the same query. For admin actions, check the role on the server from your database or session, not from anything the browser sends.Validate input and pick fields
Validate arguments with a schema (for example zod) and write only named fields. Never pass the raw form object into
data.Return only what the UI needs
Next.js serializes the return value and sends it to the browser, where anyone can read it, just like a NEXT_PUBLIC_ variable.
Use a data access layer
Put auth and database access in one server-only module, so each action stays a thin wrapper. That's the pattern the Next.js guide recommends.
'use server'
export async function deleteAccount(userId: string) {
await prisma.user.delete({ where: { id: userId } })
}'use server'
import { auth } from '@/lib/auth'
import { prisma } from '@/lib/prisma'
export async function deleteAccount() {
const session = await auth()
if (!session?.user) throw new Error('Unauthorized')
await prisma.user.delete({
where: { id: session.user.id },
})
}With Auth.js, add id to your session type so session.user.id exists.
Fix prompt you can paste
Review every file with 'use server' in this repo, including inline 'use server' functions. For each exported action that reads private data or writes to the database: add a session check at the top, take the user id from the session instead of the arguments, check that the record belongs to the caller before update or delete (or check an admin role for admin actions), validate arguments with zod and write only named fields, and return only what the UI needs. Leave sign-up, login and password-reset actions public but rate limited. List every action you changed and write a test where a logged-out user and a second user call each action directly.
How Leakcade checks this
Our server-action check reads your 'use server' files as text. It flags actions that write to the database with no visible login check, updates and deletes by a client id with no owner or admin check, and whole client objects written to the database. Sign-up, login and reset actions are left alone. Auth helpers imported from other files can hide a check from us, so the report marks these findings as heuristic and tells you what to verify.
Using next-safe-action or a similar wrapper? We skip those actions for now.
Further reading
- Next.js docs: Data security (Server Actions)
- Next.js docs: Server Actions and Mutations (Security)
- Next.js docs: Authentication
- OWASP: Insecure Direct Object Reference Prevention
- OWASP: Next.js Security Cheat Sheet
FAQ
Are Next.js server actions secure by default?
Partly. Next.js compares the request's Origin with the host to block cross-site requests, caps the body at 1 MB and encrypts action IDs. None of that checks who the user is. Auth and ownership checks are up to you.
Do I need an auth check in every server action?
In every action that reads private data or changes anything. The Next.js docs say a page-level check doesn't extend to the actions on that page.
Is middleware enough to protect server actions?
No. The Next.js docs recommend middleware (now called Proxy) only for quick, optimistic checks and a secure check close to the data, inside the action or a data access layer.
Should server actions be rate limited?
Expensive or destructive ones, yes: anything that sends email, calls a paid API or deletes data. The same tools as for login rate limiting work, keyed on the user id from the session.
Want a second pair of eyes?
We review your whole repo and write a fix prompt for every finding. $19, report within an hour.
Check my repoLeakcade is an automated check. It can miss vulnerabilities and is not a substitute for a professional security audit or pentest. Provided as is. You stay responsible for your code. Where the law allows, our liability is limited to what you paid ($19). Terms