Next.js server action security
Server actions are an elegant way to handle simple functions for common actions like form submissions, but they're a public API so you still need to consider security.
A security checklist of 7 things to improve the security of your Next.js applications. Dependencies, data validation & sanitization, environment variables, code exposure, security headers, centralizing functions, getting help from your editor.
Next.js benefits from the security protections that come with React. These are mainly related to preventing cross site scripting (XSS) because React automatically escapes string values to prevent malicious HTML from being injected. This is a good start, but is just one class of attack - production applications need more - and there are ways around it.
At Arcjet we're building a security SDK for developers and use Next.js for our dashboard app. Whilst we feel fairly safe against XSS vulnerabilities, we have implemented additional security protections on top of what Next.js provides out of the box.
This is a writeup of the approaches we've used to improve the security of Next.js. Good security comes from having a multi-layered approach - nothing is 100% secure, but there are a few easy things everyone can do to mitigate risk.
This may seem simple, but quickly applying patches is one of the most important ways to keep your application secure. Use a tool like Dependabot to get notified about new versions of your dependencies and open pull requests to update them.
Keeping your dependencies up-to-date makes it easier to react swiftly when critical security patches are released. Falling behind can create a significant technical debt that could be exploited. Otherwise, you may find yourself needing to work through multiple releases to get to the latest version in a stressful situation where a security vulnerability has been discovered.
Lock down your dependencies to specific versions by committing package-lock.json
or a similar mechanism to your repo. This ensures everyone gets the same version, avoiding subtle differences that often arise between versions. It also prevents new versions automatically being downloaded unexpectedly, which could open you to package hijacking attacks.
Running Socket can help assess the security of new dependencies and identify potential risks when new updates are released. Look for known vulnerabilities, suspicious new capabilities, trivial packages, and unexpected changes in author ownership. Running npm audit
is an alternative and you can list all your dependencies and sub-dependencies with npm ls
.
While Next.js inherits React's built-in security mechanisms, you cannot rely on them alone. Never trust data blindly – always validate input from users and external APIs.
React's escaping and sanitization help prevent XSS attacks, but they don't cover all validation needs. You still need to ensure data conforms to your application's expectations.
TypeScript's type checking is a valuable first step, but it doesn't guarantee data validity. A string might be the correct type, but it could still contain malicious content or be out of range for your application.
Use a package like zod or Valibot (or others) to validate data from users and APIs. These libraries offer schema-based validation, ensuring that incoming data matches your defined structure, types, and constraints. Don't be afraid to be strict and reject invalid input if you also provide user-friendly error messages that clearly explain what's wrong and how to fix it.
Integrate validation directly into your ORM (e.g., drizzle-zod) or API framework (e.g., tRPC) for a smooth developer experience and consistent data integrity throughout your stack.
These features have their uses, but should warrant extra caution and review:
dangerouslySetInnerHTML
in your application is a risk. This feature is explicitly named "dangerous" for a reason. It allows you to inject raw HTML directly into the DOM, bypassing React's security mechanisms. This opens the door to Cross-Site Scripting (XSS) attacks, where malicious scripts can be executed in the user's browser. Instead, construct your HTML dynamically within your components using props, state, or other safe mechanisms. This allows React to sanitize the output and protect against XSS vulnerabilities.innerHTML
to set content can expose your application to XSS attacks if not handled carefully. Prefer innerText
when setting plain text content. Unlike innerHTML
, innerText
does not parse HTML tags, preventing the execution of injected scripts.While it's generally best to avoid these features, there may be rare cases where they are necessary, such as when integrating with third-party libraries.
Next.js automatically loads environment variables and wisely defaults to restricting environment variables to server-side code. This means they are not directly exposed to the client-side, helping to prevent accidental data leaks.
If you must make a variable accessible on the client-side, prefix it with NEXT_PUBLIC_
. However, exercise caution – any value you expose this way becomes visible to anyone who inspects your website's code.
Avoid storing API keys, database credentials, or other sensitive secrets directly in environment variables. This is a security risk as environment variables can be easily exposed in various ways (e.g., through logs, crashes, or misconfigurations). Use a dedicated secrets manager like HCP Vault Secrets, AWS Secrets Manager, 1Password, Infisical or Doppler to securely store and manage sensitive credentials. These tools offer robust encryption, access controls, and auditing capabilities.
If you are self-hosting Next.js or want to add an extra layer of safety to ensure no environment variables leak into the deployed version, you can use a tool like Trufflehog to scan the build artifacts.
Server components, client components, server side rendering, server actions…these can be confusing.
Next.js has a video on static vs dynamic rendering, docs on server actions and mutations and data fetching patterns, but it’s too easy to accidentally expose server-side code to the client. This can expose credentials or cause unexpected data leaks.
One way to guard against this is to use server-only
(NPM) when you want to guarantee they will never be used on the client. If a client component tries to use a server-only module, the build will fail.
Install the package and then add this to the top of your files that should only ever execute on the server:
import "server-only";
Security headers are powerful tools that instruct browsers on how to handle your website's content. Think of them as guardrails that help prevent common web attacks.
Here are the essential headers to prioritize for your Next.js application:
default-src 'self'
) and gradually relax it as needed. Once you have a CSP policy, you can evaluate it with the CSP Evaluator tool from Google.Strict-Transport-Security
(HSTS): Forces browsers to interact with your site exclusively over HTTPS, ensuring secure communication and preventing downgrade attacks.X-Content-Type-Options
: Blocks browsers from "sniffing" content types, preventing them from misinterpreting files (e.g., treating an image as an executable script).Permissions-Policy
: This header allows you to control which browser features and APIs are available to scripts running on your site, reducing the attack surface.Referrer-Policy
: Manages how much referrer information is sent when users click links on your site. This protects user privacy.For other headers and a detailed writeup of recommended settings, check out this blog post from DarkRelay.
The next-safe package provides a simple way to configure and add these security headers to Next.js. Next.js also provides an example of how to manually set just the CSP header using middleware.
For general JS apps and those using Express, the Helmet package is a popular choice. Google's strict-csp project can also be used to set CSP headers either as a generic library or as a WebPack plugin.
The tldrsec/awesome-secure-defaults repo has a list of other useful secure-default libraries for various languages, including Node.js.
Once you have them configured, use the Security Headers tool to check everything.
Trusted Types work alongside the CSP to lock down risky JS function calls and prevent arbitrary code injection. It adds additional security when you need to use functions like eval()
or innerHTML
, however it's only supported in Chrome and Firefox.
The Trusted Types API has been around for a few years and works well with vanilla JavaScript (use Google's safevalues project). It's supported by React, but not yet with Next.js.
X-Frames-Options
: This header prevents your site from being embedded in frames on other sites, mitigating clickjacking attacks. It is now part of the Content Security Policy specification, so you can set the frame-ancestors
directive in your CSP to achieve the same effect. This header is useful for older browsers that don’t support CSP.X-XSS-Protection
: This header was a feature of Internet Explorer, Chrome, and Safari that enabled the browser’s built-in XSS filter. It is deprecated and should not be used.A well-organized security strategy involves centralizing key functions. This approach streamlines testing, auditing, and maintenance while reducing the risk of inconsistencies or oversights.
Centralized AuthN simplifies user management, reduces boilerplate code, and strengthens your defenses against unauthorized access.
Centralized AuthZ prevents accidental privilege escalation, ensures consistent enforcement of permissions, and streamlines access control management.
Centralization isn't limited to authentication and authorization. Consider consolidating other security-related functions:
Your code editor is more than just a place to write code – with the right setup, it can also help you catch security issues before you commit.
These can all be used independently, but also have editor plugins to highlight problems as you code. Catching security issues early in the development process is far less costly than fixing them after deployment!
Other resources to help you secure your Next.js application:
Server actions are an elegant way to handle simple functions for common actions like form submissions, but they're a public API so you still need to consider security.
But usually it's good enough to stop 80% of the worst actors with only 20% of the effort of doing it yourself.
How to protect GraphQL backends using Arcjet. Implementing rate limiting and bot protection for Yoga + Next.js.
Get the full posts by email every week.