Mutating Data Securely in Next.js – Server Actions, Input Validation, and CSRF Protection

Next.js uses Server Actions to handle data mutations like form submissions, database updates, and user logout. This article explains how Server Actions work, their built-in security features, how to validate client input, manage encryption keys, prevent CSRF attacks, and avoid side effects during rendering. It also includes auditing tips for secure Next.js applications.

Server ActionsInput validationEncryptionCSRF protection

~3 min read • Updated Oct 26, 2025

Introduction


In Next.js, data mutations—such as adding items, logging out, or updating a database—are handled using Server Actions. These actions are treated as public HTTP endpoints and must be secured accordingly with proper validation and authorization.


Built-in Server Actions Security Features


  • Secure IDs: Next.js generates encrypted, non-deterministic IDs for each Server Action
  • Dead Code Elimination: Unused actions are removed during build and not exposed

IDs are regenerated every 14 days or when the build cache is invalidated. Despite these protections, Server Actions should be treated like public endpoints and secured accordingly.


Validating Client Input


Client inputs such as form data, URL parameters, headers, and searchParams must be validated. Example:

// BAD: trusting searchParams directly
const isAdmin = searchParams.get('isAdmin')
if (isAdmin === 'true') return <AdminPanel />

// GOOD: verify using server-side token
const token = cookies().get('AUTH_TOKEN')
const isAdmin = await verifyAdmin(token)
if (isAdmin) return <AdminPanel />

Authentication and Authorization


Every Server Action should verify that the user is authorized to perform the mutation:

'use server'

import { auth } from './lib'

export function addItem() {
  const { user } = auth()
  if (!user) throw new Error('You must be signed in to perform this action')
  // ...
}

Closures and Encryption


Defining a Server Action inside a component creates a closure that captures outer variables. Example:

export default async function Page() {
  const publishVersion = await getLatestVersion()

  async function publish() {
    "use server"
    if (publishVersion !== await getLatestVersion()) {
      throw new Error('Version has changed')
    }
  }

  return (
    <form>
      <button formAction={publish}>Publish</button>
    </form>
  )
}

Next.js encrypts closed-over variables and generates a new private key for each build. However, encryption alone should not be relied on to protect sensitive data.


Managing Encryption Keys Across Servers


For self-hosted deployments, use NEXT_SERVER_ACTIONS_ENCRYPTION_KEY to ensure consistent encryption across servers. The key must be AES-GCM encrypted.


Preventing CSRF Attacks


Server Actions use POST requests and compare the Origin header with Host or X-Forwarded-Host. If they don’t match, the request is aborted. You can configure allowed origins:

module.exports = {
  experimental: {
    serverActions: {
      allowedOrigins: ['my-proxy.com', '*.my-proxy.com'],
    },
  },
}

Avoiding Side Effects During Rendering


Mutations should not occur during rendering. Example of incorrect usage:

// BAD
if (searchParams.get('logout')) {
  cookies().delete('AUTH_TOKEN')
}

Correct approach using Server Actions:

// GOOD
import { logout } from './actions'

<form action={logout}>
  <button type="submit">Logout</button>
</form>

Auditing a Next.js Project


  • Is there a dedicated Data Access Layer?
  • Do "use client" components receive private data?
  • Are Server Action arguments validated and re-authorized?
  • Are dynamic route params (e.g. /[param]/) validated?
  • Are proxy.ts and route.ts files audited for security?

Conclusion


Server Actions in Next.js offer a powerful way to handle mutations, but they must be secured with proper validation, authorization, and encryption. Avoid side effects during rendering, and audit your application regularly to ensure data integrity and protection.


Written & researched by Dr. Shahin Siami