Security of new features in Next.js 14 - Server Actions, Taints

Next.js 14 (and 13) introduced many attack vectors without providing the tooling necessary for organizations to detect them. It is easier than ever before to expose server secrets, introduce unauthenticated "endpoints" or any other issue that will make you vulnerable

· 4 min read
Security of new features in Next.js 14 - Server Actions, Taints

Next.js 14 (and 13 behind experimental flags) introduced many attack vectors without providing the tooling necessary for organizations to detect them. It is easier than ever before to expose server secrets, introduce unauthenticated "endpoints" or any other issue that will make you vulnerable.

What is Next.js?

Next.js is a popular open-source React framework for building server-side rendering and static web applications. It is a cool kid in the web framework space - we use Next.js too! (and we love it)

Number of stars of https://github.com/vercel/next.js

Next.js' Server Actions- controversial feature

 function Bookmark({ slug }) {
  return (
    <button
      formAction={async () => {
        "use server"

        await sql`INSERT INTO Bookmarks (slug) VALUES (${slug});`;
      }}>
      Bookmark
    </button>
  );
}

Example of Server Action

Next.js introduced Server Actions - a "new" way of bringing backend code to frontend developers' hands.

The promise is that you increase the developer's productivity by allowing them to write backend code closer to the frontend code. In reality, it increases the complexity and re-introduces solved attack vectors known for years from PHP in 2004 From a developer's perspective, it is a blessing, from a security perspective it's a nightmare.

The famous example of SQL query inside React component - no, it is not SQL injection - the SQL query is validated by sql function, but it looks horrendous.

We get it

We have to ship fast too. It is an easier way of writing code. Frontend engineers love it and they will use it whenever they can. We do not want to stop them, we want to help them.

sometimes compromise necessary or no shiney rock, mean no dinosaur meat, not good, wife firmly remind grug about young grugs at home need roof, food, and so forth ~ The Grug Brained Developer

Hidden pitfalls

Server Actions are similar to regular API endpoints - they can have broken authentication, broken access control, trust user-provided values, and return too much information.

Unlike regular endpoints - Server Actions can be defined in-line in React components making it harder to reason about. It looks like magic, and it can bite you. Developers need to think of it as regular API endpoints and treat it with similar caution.

You need to always:

  • validate input from the user
  • return only what is essential
  • check for authentication (check if a user is Bob)
  • check for authorization (check if Bob can have access to the project)

Next.js tries to solve all of the security risks by adding more layers to the onion. But more layers mean higher complexity - a field for human mistakes.

Possible solution with the Data Access Layer

So, we now understand, that the problem with Server Actions is that it gives the developer too much freedom. Next.js suggests that it can be solved by adding structure to the code and storing all "data access" code in a single place.

Data Access Layer (...) ensures consistent data access and reducing the chance of authorization bugs occurring. (...) creates a layering where security audits can focus primarily on the Data Access Layer while the UI can rapidly iterate.

The big problem with the Data Access Layer is that it relies solely on security experts to audit your code. We need automation that is going to catch the bugs before they reach production in addition to the Data Access Layer.

Server-only and Taint

import { experimental_taintObjectReference } from 'react';
 
export async function getUserData(id) {
  const data = ...;
  experimental_taintObjectReference(
    'Do not pass user data to the client',
    data
  );
  return data;
}

There is an experimental API for marking sensitive data to prevent it from reaching the browser.

It is another solution that relies on the human factor - developers or security experts to check if all sensitive data objects/values are wrapped with this function call.

The funny thing is that even if you use the Taint API you are not guaranteed to prevent the secret and data leaks...

Taint does not make you secure

Do not rely solely on tainting for security. Tainting a value doesn’t block every possible derived value... - React team

Taint does not work if you extract data fields out of this object and pass them along:

export async function Page({ searchParams }) {
  const { name, phone } = getUserData(searchParams.id);
  // Intentionally exposing personal data
  return <ClientComponent name={name} phoneNumber={phone} />;
}

Taint does not work for derived values

For example, creating a new value by upper casing a tainted string will not taint the new value:

import {experimental_taintUniqueValue} from 'react';

const password = 'correct horse battery staple';

experimental_taintUniqueValue(
  'Do not pass the password to the client.',
  globalThis,
  password
);

const uppercasePassword = password.toUpperCase() // `uppercasePassword` is not tainted

CSRF?

Yes, modern browsers use Same-Site Lax by default, but this does not mean that CSRF is impossible. When introducing new concepts we should not forget about "older" attack vectors.

Did you know that to avoid breaking single sign-on (SSO) mechanisms, Chrome doesn't enforce these restrictions (Same-Site) for the first 120 seconds on top-level POST requests? There are attack scenarios where the attacker will be able to force the session cookie refresh, thus making the CSRF possible.

CSRF should not be a huge issue in every case, but it is just another attack vector introduced by Server Actions.

Problem with the existing tooling

Yes, you can write eslint rules to catch low-hanging fruits, but it can lead you only so far. You can also use some legacy scanners like <big_scanner_name_here>, but they can't be integrated into your new and shiny Vercel or GitHub actions or cost a fortune.

Most of the scanners are simple API scanners - you give them a Swagger definition and they fuzz the API endpoints. Due to this limitation, they can't work in the paradigm of dynamic server actions.

The scanners will work best in the case of simple HTML pages or server-side Rendered pages, but what about mixing SSR and client-side rendering? You can forget about it.

Our take on this

We believe that security automation should keep up with modern concepts in web development and should be integrated as close to the development as possible - preferably in a fully automated way.

We also believe that security prevention can't rely on static code analysis - it won't catch all cases of secret leaks and sensitive data leaks. We need an automated solution that will catch all shenanigans in modern technologies (including Next.js) and that will be fast enough to run in the pipeline.

Next.js support at Vidoc - beta access

At Vidoc we want to automate your security so you can focus on what you do best. If you are interested in trying out the beta of this feature - please sign up using this form: