Research
Two in three Next.js Server Actions we scanned had no auth check in the handler
A Next.js Server Action is a public POST endpoint, and the page's session check does not extend to it. So a mutating action that does not authorize the caller inside the handler can be run by anyone who reaches the URL, directly, with no login and no UI in the way. The mechanism is well documented; how often the check is actually missing was not. We scanned public Next.js repositories and counted.
The setup, with receipts
A function marked 'use server' compiles to an HTTP endpoint that anyone can call directly with
any payload. Next.js says this in its own Data Security guide:
Server Actions "are exposed as public HTTP endpoints" and "you should treat them with the same
security considerations as public-facing API endpoints," verifying whether the user is allowed to
perform the action. Independent write-ups say it more bluntly: MakerKit's
five Server Action vulnerabilities
opens with "Server Actions are public endpoints. Full stop," and Vidoc Security Lab's
Next.js security teardown shows the
same auth-bypass shape end to end.
The reason it is easy to miss is structural. The check used to live at the route, where a
framework could guard it; Server Actions moved it into the function body, where only the author
guards it. And relying on the layer above the action is itself unsafe: the March 2025
x-middleware-subrequest bypass, CVE-2025-29927,
let a header skip Next.js middleware entirely, which is exactly why the guidance is to authorize
inside each action rather than lean on middleware.
So the mechanism is known. Practitioners even title their posts after it, like DEV's "the auth check most developers miss." The gap in the record is prevalence. The nearest published figure is adjacent, not the same: vibe-eval reports roughly 80% of AI-built apps miss auth rate-limiting, which is a different control on a different surface. We did not find a measurement of missing authorization on Server Actions specifically.
The number
Across public Next.js repositories that use a Server Action mutation, the large majority ship at least one mutating action with no session or authorization guard in the handler. Two ways to count it, and the gap between them is the point.
Read the claim precisely, because the precise version is the defensible one. We measured a code
pattern, a mutating 'use server' handler with no in-handler guard, not a confirmed vulnerability.
Some actions are unauthenticated on purpose, a sign-up or a contact form. The next number is what
separates the two.
The flagged actions are mostly the sensitive kind
If the unguarded actions were all public forms, the pattern would not matter. They are not. Of the flagged files we classified whether the action touches user-scoped or privileged data (account, profile, role, admin, payment, order) or reads like a public form (register, contact, newsletter).
Methodology
The pipeline is a static read over a public sample, characterized before it was trusted. Each step and its known bias:
- Sample. Public GitHub repositories found by code-searching for the
'use server'directive, kept only if they carrynextin package.json and contain at least one Server Action mutation. Read-only actions are excluded from the denominator. - Detector. A static read: for each
'use server'handler that mutates (create/update/delete/upsert, orINSERT/UPDATE/DELETE), we check for a session or authorization guard in the same handler window. No guard in the window, and it counts. - Precision audit. Planted fixtures only prove the regex works on clean examples, so we read a sample of flagged files from real repos for auth that sits outside the handler, in a wrapper or an action-client library like next-safe-action. In that sample none used a centralized wrapper; about three quarters had no auth signal in the file at all; the rest referenced a guard elsewhere in the file. Precision floor near three quarters, so we report the rate as a range rather than a single percentage.
- We checked our own. The withseatbelt site is a static export, which disables Server Actions, so it cannot carry this pattern. We ran the detector on it anyway and it came back clean.
- Browse only. Public source, read in memory and discarded, aggregate counts only. No repo, path, or content is retained; nothing is probed live.
What this is not
- A vulnerability rate. We measured a code pattern. The sensitivity split argues most flagged actions are the kind that should be guarded, but it does not prove any single one is exploitable.
- A framework bug. The rate is developer-side, not a Next.js default.
- A per-repo claim about who wrote the code. We do not tag individual repos as AI-written,
and deliberately so: the config that would mark it (
.cursor,CLAUDE.md) is usually gitignored, and in 2026 there is no meaningful non-AI control group to compare against. Roughly 84% of developers use or plan to use AI coding tools, and JetBrains' January 2026 survey of 10,000+ developers puts regular use at 90%. So read this as the prevalence in how Next.js is written now, which is overwhelmingly with AI assistance. We measured the pattern, not its cause. - A census. Public GitHub skews toward demos and boilerplate, which likely raises the rate versus production apps, and code search caps at a fixed result window.
- A precise figure. The sample supports "the large majority," inside the precision range above. A tight percentage needs a larger, fork-deduplicated run.
Why a single-repo scanner could not measure this
A missing check in one repo is a bug. A missing check in most repos is a property of how the code gets written, and that property does not exist inside any one repository. Single-repo tools find the missing check in your app. They cannot tell you it is common, because "common" lives in the rate across many apps, not in any one of them. The auth check on a Server Action is the kind of cross-surface invariant that is easy to drop: the endpoint is written in one place, the check belongs in another, and nothing fails when it is absent until someone calls the action directly. In the public Next.js repos we sampled, it is absent more often than not.
ConnorSeatbelt