DEV Community

Cover image for Protecting Next.js apps from CSRF attacks
Matt Angelosanto for LogRocket

Posted on • Originally published at blog.logrocket.com

Protecting Next.js apps from CSRF attacks

Written by Kevin Tomas✏️

In this article, we’re going to take a look at Cross-Site Request Forgery (CSRF) attacks in the context of a Next.js application and how to protect yourself from them. First, we will explore the concept of CSRF attacks and how they can affect a web application in general. To do this, we will describe a scenario where we will launch a CSRF attack on our Next.js app. Subsequently, we will use the package next-csrf and certain cookie security tags to show how to protect against those attacks. The source code for this post can be found here.

Jump ahead:

What is a CSRF attack?

Imagine you have logged into your online banking site, which sets a cookie in your browser to maintain your personal session. This cookie contains an authentication token that is used to identify your session and authenticate your requests.

A very simplified but possible HTTP request for sending money could be:

POST /transfer HTTP/1.1
Host: vulnerable-bank.com
Content-Type: application/json
Content-Length: 30
Cookie: session=454544

amount=1000$
name=friendlyuser@gmail.com
iban=DE7823778237873
Enter fullscreen mode Exit fullscreen mode

At the same time, in a CSRF attack, you have another tab open in your browser that has a malicious website loaded. This malicious website could contain a hidden form or JavaScript code, which submits a request to the online banking site using our authentication token.

Since the request is initiated from the same browser, the web application cannot distinguish between a legitimate request initiated by us or the forged request sent by the attacker. The web application will process the request and perform the unintended action without our knowledge or consent.

How to protect from a CSRF attack

In this section, we’ll discuss a few different ways to protect against CSRF attacks.

Using SameSite cookies

A possible way to protect your Next.js app against CSRF attacks is to define the SameSite value inside the cookies you use on your website. Google introduced this 2006 with a goal of preventing cookies from being automatically sent along with cross-site requests by the browser, as was previously the case, which would minimize the risk of confidential information being lost and provide protection against cross-site request forgery.

The SameSite attribute can take either strict or lax as its value. In strict mode, the protected cookie is not sent with any cross-site request. This already applies to clicking a simple link, but when applied to our online banking example, this implies that you would always have to re-authenticate yourself each time you’re being redirected to the online banking page.

This does not correspond to the usual behavior of web applications because users do not want to constantly log in again. Luckily, lax mode softens this behavior somewhat and allows the cookie to be sent along with some "secure" cross-site requests. This affects only the secure, read-only HTTP methods and top-level navigation (actions that cause the URL in the browser's address bar to change, such as links).

Below you can find an overview of different types of requests and their different cookie options, which will affect whether a cookie is sent or not. A thumbs up means that a cookie will be sent. For example, you can see that in strict mode, a cookie will never be sent along with a cross-site request.

Type of Request Example Without SameSite lax mode strict mode
a-tag <a href=”..”> 👍 👍 👎
form (get) <form method=”get”…> 👍 👍 👎
form (post) <form method=”post”…> 👍 👎 👎
iframe <iframe src=”..”> 👍 👎 👎
ajax $.get(“…”) 👍 👎 👎
image-tag <img src=”…”> 👍 👎 👎

Using HTTP-only cookies

By setting the HttpOnly cookie flag, you can reduce the likelihood of a CSRF attack since HTTP-only cookies cannot be retrieved by JavaScript through client-side scripts.

res.setHeader("Set-Cookie", `session=${sessionId}; Path=/;   Max-Age=600; SameSite=Strict; HttpOnly`);
Enter fullscreen mode Exit fullscreen mode

Using CSRF Tokens

One way to protect your web application against a CSRF attack is to use so-called CSRF tokens. A CSRF token is a unique random value that is generated on the server-side and is included in every request sent by the client. If the token sent by the client matches the token stored on the server side, the request is considered legitimate and gets processed by the server. Otherwise, the request will be rejected.

It is important to notice that CSRF tokens provide an effective defense against CSRF attacks as long as the token is generated randomly and cannot be easily guessed or predicted. Additionally, the token should expire after a certain period of time or after a single use, to prevent attackers from reusing old tokens.

How to perform CSRF attacks on unprotected web pages

In this section, we’re going to take a look at the code of the example online banking page and how this is vulnerable to CSRF attacks. After that, we’re going to implement the CSRF protection using the next-csrf package and setting the SameSite value in our session cookie.

Our demo online bank consists of two main routes: the login route and the transfer route. The transfer route is only accessible after a successful authentication via the login route. For that purpose, I created a simple API route to handle the login request:

// pages/api/login.js

export default function login(req, res) {
  // check the user's credentials
  const { username, password } = req.body;
  let authenticated;

  if (username === "test" && password === "123456") {
    authenticated === true 
  } else {
    authenticated === false
  }

  if (authenticated) {
    // set a cookie with the a random sessionId
    const sessionId = 454544;
    res.setHeader("Set-Cookie", `session=${sessionId}; Path=/;   Max-Age=600`);

    // send a success response
    res.status(200).json({ message: "Login successful" });
  } else {
    // send an error response
    res.status(401).json({ message: "Invalid credentials" });
  }
}
Enter fullscreen mode Exit fullscreen mode

The login page looks like this: The login page

The most important line of code in the code above is probably: res.setHeader("Set-Cookie",session=${sessionId}; Path=/; Max-Age=600);

This sets a cookie with a session ID and a duration of 10 minutes. For the sake of simplicity, we use hardcoded session IDs, usernames, and passwords.

After a successful authentication, you should see the transfer page of our demo online banking site: Money transfer page

The corresponding simplified API route for handling bank transfers looks like this:

// pages/api/transfer.js

export default function handler(req, res) {
  // Check that the request method is POST
  if (req.method !== 'POST') {
    res.status(405).json({ error: 'Method Not Allowed' });
    return;
  }

  // Check that the request has a valid session cookie
  if (!req.cookies.session) {
    res.status(401).json({ error: 'Unauthorized' });
    return;
  }

  // Parse the JSON data from the request body
  const { amount, name, iban } = req.body;

  // TODO: Implement transfer logic

  // Return a success message
  res.status(200).json({ message: 'Transfer successful' });
}
Enter fullscreen mode Exit fullscreen mode

In the code from above we perform two checks: One for the request method and one checks the session cookie.

We are not going to create a malicious website; instead, we’ll simulate data being sent via a form or JavaScript code on that malicious website. Since the request is initiated from the same browser, the session cookie will be automatically attached to it and our backend won’t be able to distinguish between a legitimate request initiated by us as an authenticated user or a forged request sent by an attacker.

All we need to send via this CURL request is the form data (amount, name, iban) and the session cookie. The corresponding request looks like this:

curl -X POST \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -H "Cookie: session=1234" \
  -d "iban=1736123125&amount=10000000&name=Criminal" \
  http://localhost:3000/api/transfer
Enter fullscreen mode Exit fullscreen mode

The unprotected api/transfer route will lead to an answer like this:

{"name":"Criminal","iban":"1736123125","amount":"10000000"}
Enter fullscreen mode Exit fullscreen mode

That response means that we just successfully executed a CSRF attack on the online banking page.

How to protect a Next.js app from CSRF attacks

Using SameSite and HttpOnly tokens

Let’s first implement the SameSite and the HttpOnly attributes of our session cookie, since this is easily done in one step. Remember that we set the cookie in our login API route located in src/pages/api/login.js. Let’s adjust the setting of the cookie in the corresponding route:

res.setHeader("Set-Cookie", `session=${sessionId}; Path=/;   Max-Age=600; SameSite=Strict; HttpOnly`);
Enter fullscreen mode Exit fullscreen mode

That’s all you need to do in order to configure your session cookie. Whether you choose to use a strict or lax policy depends on how high your safety requirements are and how much you’re willing to sacrifice in terms of user experience.

Using CSRF Tokens

As mentioned in the sections above, there is a package called next-csrf that allows us to easily implement the following steps to ensure protection from CSRF attacks:

  1. The server generates and sends the client a csrf token
  2. The client/browser submits a form with the token
  3. Server checks whether the token is valid or not

To successfully launch a CSRF attack, the attacker would need to obtain the CSRF token from your website and use JavaScript to access it. This means that if your website does not allow Cross-Origin Resource Sharing (CORS), the attacker cannot access the CSRF token, effectively neutralizing the threat.

In order to install the next-csrf package, run the following command in the root of your Next.js project:

npm i next-csrf --save
Enter fullscreen mode Exit fullscreen mode

In the first step, let’s initialize next-csrf by creating a setup file. This will create middleware for creating and validating the CSRF tokens:

// "lib/csrf"
import { nextCsrf } from "next-csrf";

const { csrf, setup } = nextCsrf({
 // eslint-disable-next-line no-undef
 secret: "12345",
});

export { csrf, setup };
Enter fullscreen mode Exit fullscreen mode

In a production environment, you would of course store your secret in an environment file.

In order to set up the CSRF token, we will use a server-side-rendered page like our login page because you are using CSRF mitigation to harden your requests from authenticated users.

import Head from "next/head";
import { setup } from "lib/csrf";

export default function Home() {
 return (
   ...
 );
}

export const getServerSideProps = setup(async ({ req, res }) => {
 return {
   props: {},
 };
});
Enter fullscreen mode Exit fullscreen mode

After that, the only thing we need to do to protect an API route is to wrap the corresponding API route with our csrf middleware:

// src/pages/api/transfer.js
import { csrf } from "../../../lib/csrf";

const handler = (req, res) => {
   // Check that the request method is POST
   if (req.method !== 'POST') {
     res.status(405).json({ error: 'Method Not Allowed' });
     return;
   }
    // Check that the request has a valid session cookie
   if (!req.cookies.session) {
     res.status(401).json({ error: 'Unauthorized' });
     return;
   }
    // Parse the JSON data from the request body
   const { name, iban, amount } = req.body;
   console.log(name, iban, amount)
   console.log(req.cookies.session);
    // Return a success message
   res.status(200).json({ name, iban, amount });
 }

 export default csrf(handler);
Enter fullscreen mode Exit fullscreen mode

Before the actual request logic takes place, the csrf middleware will perform the validation of the CSRF tokens and in case of failed validation, it will throw an error:

{"message":"Invalid CSRF token"}
Enter fullscreen mode Exit fullscreen mode

Conclusion

In this blog post, we covered the topic of protecting your Next.js app against CSRF attacks and had a detailed look at the next-csrf package, which allows you to implement CSRF mitigation through CSRF tokens. In addition, we had a look at the configuration of your cookies and how to enhance the security by setting certain cookie values.


LogRocket: Full visibility into production Next.js apps

Debugging Next applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket signup

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Next app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your Next.js apps — start monitoring for free.

Top comments (0)