Every exported Next.js Server Action is a public POST endpoint, and the authentication check on the page that renders the form does not protect it. Next.js says this plainly in its data security guide, yet missing checks inside actions, actions that trust IDs and ownership fields sent by the client, and actions that return whole database records remain the most common App Router flaws. This guide goes deeper than the Server Actions section of our React and Next.js guide. It covers how actions are exposed and what the framework does and does not protect, how to design a Data Access Layer that makes authorisation hard to forget, input and return value rules, origin checks and the encryption key behind closures, the taint APIs and their limits, logging, testing and a rollout plan. It is for developers, reviewers and application security engineers working on Next.js App Router applications, and reflects the Next.js documentation as read on 27 October 2026.
Key takeaways
- Authorise inside every action. A Server Action is a separate entry point; render-time gating and page redirects are UI, not security. Check the session and the user's right to the specific resource in the action or the layer it calls.
- Accept references, not facts. Let the client say which record and what change; re-read ownership, price, role and status from a trusted source using the session.
- Put all data access behind a server-only layer. A Data Access Layer that verifies the session, checks permissions and returns minimal DTOs turns authorisation into a property of the codebase rather than of each developer's memory.
- Framework protections are a seatbelt, not a lock. Encrypted action IDs, dead code elimination, the Origin check, the 1MB body limit and closure encryption all reduce exposure, and Next.js says none replaces application checks.
- Patch the framework layer fast. CVE-2025-29927 bypassed middleware authorisation and CVE-2025-55182 gave unauthenticated code execution through React Server Components; both show why checks belong close to the data and why RSC packages need emergency patching.
How Server Actions are exposed
A function marked with 'use server' stays on the server. At build time the compiler replaces it in client bundles with a reference, an action ID plus a dispatcher, that POSTs back to the page. The Next.js server actions guide puts the consequence directly: the implementation stays on the server, "but the route is reachable to anyone who can send the same POST". Even an action that nothing in your UI imports is callable if it is exported and referenced.
| Framework protection | What it does | What it does not do |
|---|---|---|
| Encrypted, non-deterministic action IDs | IDs are generated at compilation, cached for at most 14 days and regenerated on a new build | Hide an action from anyone who loads the page that uses it; the ID is in the client bundle |
| Dead code elimination | Unused actions are removed from client bundles and get no public endpoint | Protect actions that are referenced anywhere, including admin-only forms |
| POST only, plus Origin check | Compares the Origin host to X-Forwarded-Host or Host and rejects mismatches | Stop non-browser clients, which can set any headers; the serverActions reference notes that a request with no Origin header is allowed through with a warning |
| Body size limit | Caps action request bodies at 1MB by default | Rate-limit calls or stop many small expensive requests |
| Closure variable encryption | Encrypts variables captured by inline actions before they reach the client, with a key generated per build | Make it safe to capture secrets; Next.js does not recommend relying on encryption alone |
| No side effects during render | Next.js blocks setting cookies and revalidating caches inside render | Prevent mutations inside actions that lack checks |
Two behaviours affect design. Next.js dispatches actions one at a time per client, but the server does not queue them, so concurrency limits, idempotency and race conditions still need server-side handling. And an action's return value travels in the same Flight response as any re-rendered RSC payload, so anything the action returns, or any data the re-rendered route reads, reaches the browser.
Designing the Data Access Layer
Next.js recommends three data approaches and advises choosing one: existing HTTP APIs treated with a zero trust model for large established estates, a dedicated Data Access Layer (DAL) for new projects, and component-level access only for prototypes. The DAL is where most of the security value lies.
What the DAL must do
- Run only on the server: start each DAL module with
import 'server-only'so importing it from a Client Component fails the build. The data security guide notes this works in'use server'files too. - Own the secrets: Next.js recommends that only the DAL reads
process.env, keeping database credentials and API keys out of components. - Verify the session itself: every exported DAL function obtains the current user from the session (cached per request) rather than accepting a user ID parameter from callers.
- Check permission for the specific object: load the record, compare its owner, tenant or ACL to the session user, and fail loudly. This is the IDOR control Next.js calls out (ASVS V8.2.2).
- Return DTOs: map rows to objects containing only the fields the caller renders (ASVS V15.3.1). Never return ORM entities, which carry password hashes, internal flags and relations.
Patterns that make mistakes hard
| Pattern | How it helps |
|---|---|
One function per use case (getInvoiceForCurrentUser(id), markInvoicePaid(id)) rather than generic getInvoice(id) | The authorisation rule is part of the name and cannot be skipped by a caller |
Lint or dependency rules that forbid importing the database client or process.env outside /dal | Matches the Next.js audit advice to verify database packages and environment variables are not imported outside the DAL |
Thin 'use server' actions that parse input, call one DAL function and shape the response | Reviewers can see at a glance that each action delegates to an authorised path |
| Tenant ID taken from the session, never from arguments | Cross-tenant access requires breaking the session, not editing a field |
| Separate DAL functions for privileged operations with step-up checks | Next.js suggests elevated session checks or re-authentication for destructive operations such as deletes |
See the React and Next.js Security Guide for optimistic versus secure session checks and cookie settings, and the React Server Components Security Guide for the RSC serialisation model.
Inputs: what an action may trust
Next.js lists form data, URL parameters, headers and search parameters as untrusted, and its audit checklist adds that every [param] folder is user input. The server actions guide is more specific: the client "legitimately tells the server which item to act on, but it should not supply the row's contents or ownership".
- Parse with a schema (zod or similar) at the start of the action, rejecting unknown keys. Remember that a schema only checks shape: a well-formed object can still refer to a row the user does not own.
- Reduce the input to a reference and a change: an ID plus the fields the user may edit. Look up everything else, such as owner, price, plan, role or status, from the database using the session.
- Treat bound arguments as input: values passed with
.bind()or captured by a closure come back from the client. Closure values are encrypted, but they are still the client's copy of a past render, so re-validate anything that matters. - Constrain sizes and counts: keep the 1MB default unless an action genuinely needs more, and raise
serverActions.bodySizeLimitper application rather than globally. - Rate limit expensive actions: email sending, payment intents, invitations and search should have per-user and per-IP limits (ASVS V2.4.1). See the Rate Limiting and API Abuse Guide.
See the Input Validation and Output Encoding Guide for schema design.
Outputs: controlling what reaches the browser
- Return the minimum: Server Action return values are serialised to the client. Return
{ ok: true }, a validation error map or a small DTO, not the updated record. - Watch the re-render: calling
updateTag,revalidatePath,refresh,redirector changing cookies re-renders the current route in the same response. If that route's Server Components pass full objects to Client Components, the leak happens there. - Keep secrets off
NEXT_PUBLIC_: any variable with that prefix is exposed to the client. - Use error shapes that do not leak: catch database and upstream errors in the DAL and return generic messages; log the detail server-side.
Taint APIs: a second net with holes
With experimental: { taint: true } in next.config, React's experimental_taintObjectReference and experimental_taintUniqueValue make React throw if a marked object or value crosses to a Client Component. The flag also taints the process.env object. The Next.js reference lists caveats that matter in review:
| Caveat | Consequence |
|---|---|
| Objects are tracked by reference | A spread or copy ({ ...user }) is untainted and passes freely |
The process.env taint covers the object only | Passing process.env.API_KEY as a string, or a copy of the object, is not caught |
| Derived values are not tracked | A substring, hash or formatted version of a tainted token must be tainted separately |
| Taint lasts for the lifetime of the reference | Values re-created later are not covered |
| The feature is experimental and enables React's experimental channel for the app directory | Assess the stability trade-off before production use |
Use taint for tokens, API keys and full user records as a backstop, and keep DTO filtering in the DAL as the primary control, as both the Next.js reference and its warning advise.
Origins, CSRF and the encryption key
allowedOrigins
Server Actions are POST only and compare the Origin host with X-Forwarded-Host or Host. The serverActions.allowedOrigins option in next.config adds extra hosts. The reference documents the matching rules precisely:
- Host only: entries match the host of the
Originheader, meaning hostname plus port if present.my-proxy.comdoes not matchmy-proxy.com:8443. - Wildcards:
*stands for exactly one label and**for one or more, only at the start of a pattern. Partial labels (app-*.example.com) and port wildcards are not supported. - Behind a proxy: no entry is needed if the proxy forwards the public host in
x-forwarded-host. Add the host users see in the address bar, not the internal one. - It applies in production: the check runs in development and production alike.
Keep the list minimal and avoid ** on domains where other teams or customers can host content. The Origin check complements, but does not replace, SameSite session cookies. See the CSRF Prevention Guide.
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY
By default Next.js generates a new private key for each action at every build, so closures and action references only work for that build. Self-hosted fleets running several instances can set NEXT_SERVER_ACTIONS_ENCRYPTION_KEY to a base64-encoded AES key of 16, 24 or 32 bytes (32 by default) so all instances agree and references survive rolling deployments.
- Treat it as a production secret: store it in a secrets manager, inject it at runtime, limit who can read it, and do not reuse it across environments. Next.js tells teams to follow standard practices such as key rotation.
- Rotate deliberately: a new key invalidates in-flight references, which surface as "Failed to find Server Action" errors. Rotate with a rolling deployment and a UI retry path, as Next.js recommends for new builds.
- Do not let it become a reason to capture secrets: a stable key across builds makes captured values decryptable for longer. Keep secrets out of closures entirely. See the Secrets Management Guide.
Proxy, Route Handlers and framework vulnerabilities
The Next.js audit checklist says proxy.ts (formerly middleware) and route.ts files "have a lot of power" and deserve traditional review and penetration testing. Two incidents explain why checks should sit in the DAL rather than only at the edge.
| Vulnerability | What happened | Fixed in | Lesson |
|---|---|---|---|
| CVE-2025-29927 (GHSA-f82v-jwr5-mffw), CVSS 9.1 | Authorisation checks implemented in Next.js middleware could be bypassed; the published workaround was to block external requests carrying the x-middleware-subrequest header | 12.3.5, 13.5.9, 14.2.25, 15.2.3 | Middleware or proxy checks are optimistic; the DAL must check again |
| CVE-2025-55182, CVSS 10.0, in the CISA KEV catalogue since 5 December 2025 | Unauthenticated remote code execution by sending a crafted payload to a Server Function endpoint in React Server Components 19.0 to 19.2.0; RSC apps could be affected even without their own Server Functions | React 19.0.1, 19.1.2, 19.2.1 and the patched Next.js releases React lists per minor line | RSC packages are internet-facing parsers; patch within days and track follow-ups |
| Follow-ups CVE-2025-55184, CVE-2025-67779, CVE-2026-23864 (denial of service) and CVE-2025-55183 (source code exposure) | Further RSC flaws disclosed in December 2025 and January 2026 | See React's advisories | Expect a stream of fixes in a young protocol layer |
Route Handlers have none of the Server Action extras, so apply ordinary API controls to them. Map exploitation of either to T1190 Exploit Public-Facing Application.
Where identity fits
Server Actions often act with the application's own credentials: a database user, a payments key, a cloud storage role. When an action lacks a user-level check, the caller borrows the application's non-human identity. Scope those credentials to what the application needs, prefer short-lived workload identity over static keys for cloud resources, and keep the DAL as the only code that can use them. See the Vercel Tokens Security Guide for deployment-platform credentials.
Detection, testing and rollout
What to log
- Every action invocation: action name, session user, tenant, target resource ID, outcome and latency, as structured logs. ASVS V16.3.2 asks for failed authorisation attempts to be logged.
- Authorisation denials from the DAL, with enough context to spot enumeration (many IDs, one user).
- Origin check failures and requests without an
Originheader to action endpoints. - Body size rejections and rate limit hits, which point to automation.
How to test
- Inventory actions: list every
'use server'file and inline action, with the DAL function each calls. - Replay outside the UI: capture a legitimate action request in browser developer tools and replay it with no session, with another user's session and with another tenant's IDs. Each should fail.
- Tamper with arguments: change IDs, add unexpected fields such as
roleorownerId, and send oversized payloads. - Inspect responses: confirm return values and re-rendered payloads contain only DTO fields.
- Automate: add integration tests for unauthorised and cross-tenant calls to CI, and a lint rule that fails when a
'use server'file imports the database client directly.
Rolling out
- Patch Next.js and React to current fixed releases first.
- Build the DAL for the most sensitive domain (billing, admin, user data), move its actions to delegate to it, and add the denial tests.
- Enable import restrictions in warn mode, fix violations, then switch to error.
- Turn on taint for tokens and user records in staging, watch for errors that reveal leaks, then enable in production if the experimental channel is acceptable.
- Roll back by redeploying the previous build; keep
NEXT_SERVER_ACTIONS_ENCRYPTION_KEYunchanged during the rollout so references stay valid.
Decision guidance: small apps can enforce the pattern with a single DAL module and a lint rule. Large organisations with separate backend teams may be better served by keeping a zero trust HTTP API behind Server Components, as Next.js suggests, so authorisation lives in one service regardless of front end. Avoid mixing approaches in one codebase.
Metrics
- Action coverage: share of Server Actions that delegate to a DAL function with an authorisation check. Target 100%.
- Denial test coverage: share of actions with automated unauthenticated and cross-user tests.
- DTO compliance: number of actions or Server Components passing ORM entities to the client, target zero.
- Patch latency: days from a Next.js or React security release to production.
- Import violations: database or
process.envimports outside the DAL, target zero.
Common failure patterns
- Relying on the page's auth check: the page redirects anonymous users, the action behind its form does not check, and a direct POST succeeds.
- Checking login but not ownership: any signed-in user can update any record by changing the ID.
- Trusting client-sent fields: price, role, plan or
userIdaccepted fromFormDatabecause the schema said the shape was valid. - Returning the updated record: the response carries password hashes, tokens or internal flags to the browser.
- Security in middleware only: a proxy bypass such as CVE-2025-29927 removes the only check.
- Over-broad allowedOrigins:
**.example.comwhere user content or partner sites live under that domain.
Framework mapping
| Framework | Reference | Control in this guide |
|---|---|---|
| OWASP ASVS 5.0 | V8.2.1, V8.2.2, V8.3.1 | Function and object-level authorisation enforced in the DAL |
| OWASP ASVS 5.0 | V2.2.2, V15.3.1, V2.4.1 | Server-side validation, minimal DTOs, rate limiting |
| NIST SP 800-53 Rev. 5 | AC-3, AC-4, SI-10 | Access enforcement, controlling data flow to the client, input validation |
| NIST SP 800-53 Rev. 5 | SC-12, AU-12, SA-11 | Encryption key management, action audit records, developer testing |
| NIST CSF 2.0 | PR.AA-05 | Authorisation defined and enforced with least privilege |
| OWASP API Security Top 10 2023 | API1:2023 Broken Object Level Authorization | Ownership checks on every action; see the OWASP API Top 10 API1 Broken Object Level Authorization Guide |
| MITRE ATT&CK v20 | T1190 | Exploitation of framework flaws and unprotected actions |
Practitioner checklist
- Inventory every Server Action and confirm each authenticates and authorises inside the action or its DAL call.
- Build a
server-onlyDAL that reads the session itself, checks object ownership and returns minimal DTOs. - Restrict database clients and
process.envto the DAL with lint or dependency rules. - Parse inputs with strict schemas and re-read ownership and sensitive fields from trusted sources.
- Return only what the UI renders, and check re-rendered routes for full objects passed to Client Components.
- Keep
allowedOriginsminimal and exact, and keepbodySizeLimitat the default unless needed. - Store
NEXT_SERVER_ACTIONS_ENCRYPTION_KEYin a secrets manager per environment and keep secrets out of closures. - Enable taint for tokens and user records as a backstop, knowing its reference-only limits.
- Add direct-call, cross-user and cross-tenant tests for actions to CI.
- Patch Next.js and React security releases within days, and never rely on proxy or middleware checks alone.
Standards and references
- Next.js: How to think about data security in Next.js
- Next.js: Server Actions and Mutations
- Next.js: serverActions configuration
- Next.js: taint configuration
- GitHub Advisory Database: CVE-2025-29927 Authorization Bypass in Next.js Middleware
- React: Critical Security Vulnerability in React Server Components
- OWASP: Application Security Verification Standard 5.0
- CISA: Known Exploited Vulnerabilities Catalog
Related NHI Mgmt Group resources: React and Next.js Security Guide · React Server Components Security Guide · CSRF Prevention Guide · API Security Guide