HomeBlogSecurity Nextjs Applications

A safe approach to improve the security of your Next.js Server Actions

Published Dec 6, 2024
Updated Dec 15, 2024
2 minutes read

Introduction

I was working on a research for a blog post about security vulnerabilities in Next.js applications and a good approach to improve the security of your Next.js Server Actions. Looking on the internet, and a few open-source projects, I found a library called next-safe-action. This library is a middleware for Next.js that helps you to secure your server actions. This blog post I'll cover simple steps to improve the security of your Next.js Server Actions.

Why Security is Important in Server Actions

Server Actions are functions that run on the server-side of your application. Also, they can be called in Server and Client Components to handle form submissions and data mutations in Next.js applications. Due to the simplicity of the Next.js API, it's easy to create Server Actions that are vulnerable to security attacks. For example, a Server Action that doesn't validate the input data can be exploited by an attacker to execute malicious code on your server. To prevent these attacks, it's important to secure your Server Actions by validating the input data, sanitizing the output data, and protecting sensitive data.

How to Secure Your Server Actions using next-safe-action

First, you need to install the next-safe-action library in your Next.js project:

npm install next-safe-action

Then, you can use the createSafeActionClient function to create a secure Server Action:

// "lib/safe-action.ts"
import { createSafeActionClient } from 'next-safe-action'
import { zodAdapter } from 'next-safe-action/adapters/zod'
import { z } from 'zod'
 
// Your auth function (e.g., using NextAuth.js)
import { auth } from '@/auth'
 
export class ActionError extends Error {}
 
/** Basic action client */
export const actionClient = createSafeActionClient({
  defineMetadataSchema() {
    return z.object({
      actionName: z.string(),
    })
  },
})
 
// The authAction is a secure Server Action that requires the user to be authenticated.
export const authAction = actionClient.use(async ({ next }) => {
  const session = await auth()
 
  if (!session) {
    console.error('Unauthorized')
    throw new ActionError('Unauthorized')
  }
 
  return next({
    ctx: {
      session,
    },
  })
})

In the above example code, we define a authAction that requires the user to be authenticated. The auth function is an authentication function that returns the user session if the user is authenticated. If the user is not authenticated, the authAction throws an ActionError with the message "Unauthorized". To use the authAction, we can import it in our Server Actions and call it, here is an example:

// "app/private-page/action.ts"
'use server'
 
import { z } from 'zod'
 
import { logger } from '@/lib/logger'
import { authAction } from '@/lib/safe-action'
 
const schema = z.object({
  userId: z.number(),
})
 
export const deleteUser = authAction
  .metadata({ actionName: 'deleteUser2' })
  .schema(schema)
  // The action to delete a user
  .action(async ({ parsedInput: { userId } }) => {
    logger.info(`Deleting user with id: ${userId}`)
    await new Promise((res) => setTimeout(res, 1000))
    logger.info(`User ${userId} deleted`)
  })

Then, the deleteUser server action can be called, and it will require the user to be authenticated. If the user is not authenticated, the authAction will throw an ActionError with the message "Unauthorized".

Here a basic example using a client component:

// "app/private-page/delete-user.tsx"
'use client'
 
import { deleteUser } from './action'
 
const getUserId = () => 1993
 
export function DeleteUser() {
  const userId = getUserId()
  const handleDelete = async () => {
    await deleteUser({ userId })
  }
  return (
    <button className="bg-blue-400 text-white p-2 rounded-md" onClick={handleDelete}>
      Hello safe Server Action
    </button>
  )
}

And now we can use this client component in our page:

// "app/private-page/page.tsx"
import { DeleteUser } from './delete-user'
 
export default async function SecureActionPage() {
  return (
    <>
      <h1>Delete the user</h1>
      <DeleteUser />
    </>
  )
}

I have a full example in this repository, feel free to check it out.

Conclusion

This blog post was a quick introduction to the next-safe-action library and how you can use it to secure your Server Actions in Next.js applications. By validating the input data, sanitizing the output data, and protecting sensitive data, you can prevent security attacks and keep your application safe. If you want to learn more about the next-safe-action library, you can check the official documentation. Also, I recommend reading the Next.js Security Best Practices to learn more about how to secure your Next.js applications and a blog post that I wrote about Security in Next.js Applications.