The magic of Next.js Server Actions is a security nightmare if misunderstood. The framework obscures the network boundary. You write a function, you call it from a client button, and it just works. But under the hood, Next.js compiles that function into an HTTP POST endpoint. Attackers do not click your UI buttons. They find the exposed endpoint, craft custom HTTP requests, and attack your backend directly. We exploit Server Actions in almost every Next.js penetration test we perform.

The core vulnerability: Implicit trust in Server Actions

Because Server Actions look like standard JavaScript functions inside your React files, developers skip basic security checks. They write code assuming that if a button is disabled in the user interface, the action cannot be triggered. They assume that if a route is protected by Next.js middleware, the Server Actions on that route are also protected. This is fundamentally false.

Server Actions are publicly accessible endpoints. During an audit, we capture the Action ID from the client-side JavaScript bundle (often a hash string). We craft raw HTTP POST requests with the Next-Action header. If the action does not explicitly validate user sessions internally, we trigger operations without logging in. We bypass your middleware entirely.

Failure example

A Server Action trusts a user ID from the request

An action such as updateUserEmail(userId, email) lets the caller choose the account. A valid user replays the request with a different user ID. The server executes the update because the developer assumed only the UI could pass the ID. The fix is to read the user ID exclusively from the verified server session context and reject any modification the session cannot access.

Auditing parameter inputs with schemas

When users submit forms, Next.js handles serialization. If your Server Action accepts arguments directly and passes them to Prisma or raw database queries without validation, you are vulnerable to type injection, NoSQL injection, and mass assignment attacks.

Attackers send nested JSON objects instead of strings. If you expect an email string but receive an object like { "contains": "@" }, your ORM might interpret this as a wildcard query, exposing data you did not intend to leak.

Always validate the input schema inside the server function. Use libraries like Zod or Yup to enforce strict type structures. Drop unknown keys. Never pass raw action arguments directly into a database driver.

// SECURE: Validating user session and payload in Next.js Server Action
"use server";

import { getSession } from "@/lib/session";
import { z } from "zod";

const profileSchema = z.object({
  displayName: z.string().min(3).max(50)
});

export async function updateProfile(data) {
  // 1. Enforce authentication inside the action
  const session = await getSession();
  if (!session?.userId) {
    throw new Error("Access Denied");
  }

  // 2. Enforce strict type validation
  const parsed = profileSchema.parse(data);

  // 3. Bind the database write to the session context, NOT client arguments
  return await db.user.update({
    where: { id: session.userId },
    data: { name: parsed.displayName }
  });
}

Data leakage through Server Action return values

Another critical flaw we exploit is over-fetching. A Server Action might query a database for a user record and return the entire object to the frontend, assuming the React component will only render the username. The HTTP response actually contains the full object, including password hashes, reset tokens, and internal role flags. We intercept the response in Burp Suite and extract the sensitive data. Server Actions must strictly filter their return values before serialization.

Data Transfer Objects (DTOs) are mandatory. Before returning any data from a Server Action, map the database record to a strict DTO that only contains the fields the client explicitly requires. Zod can be used for output validation just as it is used for input validation.

Next.js Security Checklist

If your application uses Next.js Server Actions, verify these defensive controls:

  1. Check authorization inside actions: Do not rely on middleware or UI visibility. Middleware routes requests; it does not secure Server Actions reliably due to how Next.js handles POST routing. Every action must check user sessions independently.
  2. Use strict Zod schemas: Validate all incoming payloads to prevent type manipulation. Strip unknown fields using `.strip()` or `.strict()`.
  3. Sanitize HTML outputs: If the action returns user-submitted text that is rendered in React using `dangerouslySetInnerHTML`, prevent cross-site scripting (XSS) by running the data through `DOMPurify` or `sanitize-html`.
  4. Enforce CSRF protections: Next.js implements CSRF protection for Server Actions by checking the Origin header, but if your application allows CORS from multiple subdomains, you must validate origins manually inside critical actions.

Exploiting the Next.js Data Cache

Next.js aggressively caches data by default. This is excellent for performance, but disastrous for security if misconfigured. Developers often use `fetch()` inside a Server Action to pull a user's private data from an external microservice. If they forget to set { cache: 'no-store' }, Next.js will cache the response globally. The next user who triggers that action will receive the first user's private data.

We test for this by triggering an action that fetches an account balance as User A, and immediately triggering the same action as User B. If User B receives User A's balance, we have identified a critical data leakage vulnerability. Server Actions that handle PII or financial data must explicitly opt out of caching.

Prototype Pollution and Mass Assignment in Next.js ORMs

When Server Actions accept complex JSON objects and pass them directly into Prisma or TypeORM `update()` methods, we test for Mass Assignment. If a user profile form submits { "displayName": "Simpa" }, we modify the intercept payload to { "displayName": "Simpa", "role": "ADMIN", "balance": 999999 }. If the Server Action does not explicitly filter these keys using a strict Zod schema, the ORM writes the elevated role and fraudulent balance directly to the database.

Furthermore, we test for Prototype Pollution. We inject `__proto__` or `constructor.prototype` keys into the JSON payload. If the server-side code merges this payload with an existing object without proper sanitization, we can pollute the global object prototype, leading to Denial of Service (DoS) or even Remote Code Execution (RCE) on the Node.js server.

Run a complete Server Action test

  1. List every action. Search the codebase for "use server". Inspect generated client bundles. Record the component, action name, input fields, and expected role. If you find orphaned actions (code that is no longer used by the UI but still exported), delete them. They are active endpoints.
  2. Capture a valid request. Save the headers, action identifier (`Next-Action`), serialized body, cookies, response, and the record changed by the action.
  3. Remove authentication. Replay the request without cookies, with an expired session, and with a session for a lower role. Each request must fail before any database read or write begins.
  4. Change ownership fields. Replace user IDs, tenant IDs, order IDs, and hidden form values. Verify the server checks access to the exact record using Insecure Direct Object Reference (IDOR) tests.
  5. Change the data shape. Add unknown fields, nested objects, duplicate keys, long values, and the wrong types. The schema must reject the request without storing partial data or crashing the Node process.
  6. Repeat the request. Send the same action twice and send parallel copies. Payment, invite, password-reset, and credit actions need strict idempotency rules to prevent race conditions.

Keep the action inventory, raw requests, server logs, database before-and-after values, and a retest response. These records prove the failed request caused no write. Use the same checks on Next.js Route Handlers (`route.ts`). See the BOLA repair guide for the record-level pattern.

Let Simpa Labs audit your Next.js application

Modern React stacks are highly dynamic and blur the lines between frontend and backend. We review your Server Actions, test your API routes, audit your database connections, and uncover the logical flaws that automated scanners miss completely.

Book a Next.js Pentest