Back to Labs

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.

100500%75%per repo68%per actionn = 20 repos. Independent sample of 33: 84% per repo.
per repo counts a repo once if any of its actions is unguarded, which a repo with more actions trips more easily by chance. per action is the unguarded share of all mutating actions. It removes that size effect, it is lower, and it is the one we trust.

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).

100500%80%sensitive20%unclear0%public formn = 15 flagged actions. "sensitive" = touches account, role, payment, order data.
None of the flagged actions in the sample read as a public form. Most reference the kind of entity that should sit behind an authorization check. This is a keyword heuristic, not proof any single action is exploitable, but it moves the finding from "no auth check present" toward "missing a check it should have."

Methodology

The pipeline is a static read over a public sample, characterized before it was trusted. Each step and its known bias:

What this is not

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.