CSRF in Next.js Apps: When Server Actions and Cookies Become a Security Risk
What is CSRF and How Does it Affect Next.js Apps
Cross-Site Request Forgery (CSRF) is a type of attack that tricks a user into performing unintended actions on a web application that they are authenticated to. In the context of Next.js apps, CSRF can be particularly problematic when server actions and cookies are involved.
When a user interacts with a Next.js app, the app may use cookies to store sensitive information, such as authentication tokens or session IDs. If an attacker can trick the user into sending a request to the app, they may be able to exploit this sensitive information to perform unintended actions.
For example, consider a Next.js app that allows users to transfer funds from their account to another account. If an attacker can trick the user into sending a request to the app to transfer funds, they may be able to steal the user's money.
To illustrate this, let's consider an example of a vulnerable Next.js page:
import { useState } from 'react';
export default function TransferFunds() {
const [amount, setAmount] = useState(0);
const [recipient, setRecipient] = useState('');
const transferFunds = async () => {
const response = await fetch('/api/transfer-funds', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ amount, recipient }),
});
const data = await response.json();
// Handle the response data
};
return (
setAmount(e.target.value)} />
setRecipient(e.target.value)} />
);
}
In this example, the transferFunds function sends a POST request to the /api/transfer-funds endpoint to initiate the fund transfer. However, this request is vulnerable to CSRF attacks because it relies on the user's cookies to authenticate the request.
How to Protect Against CSRF Attacks in Next.js Apps
To protect against CSRF attacks in Next.js apps, you can use a technique called token-based validation. This involves generating a unique token for each user session and including it in the request payload.
Here's an example of how you can implement token-based validation in the transferFunds function:
import { useState, useEffect } from 'react';
export default function TransferFunds() {
const [amount, setAmount] = useState(0);
const [recipient, setRecipient] = useState('');
const [csrfToken, setCsrfToken] = useState('');
useEffect(() => {
const token = Cookies.get('csrf-token');
setCsrfToken(token);
}, []);
const transferFunds = async () => {
const response = await fetch('/api/transfer-funds', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ amount, recipient, csrfToken }),
});
const data = await response.json();
// Handle the response data
};
return (
setAmount(e.target.value)} />
setRecipient(e.target.value)} />
);
}
In this example, we generate a unique CSRF token for each user session and include it in the request payload. We also validate the token on the server-side to ensure that the request is legitimate.
On the server-side, you can validate the CSRF token using a middleware function:
import { NextApiRequest, NextApiResponse } from 'next';
const csrfMiddleware = async (req: NextApiRequest, res: NextApiResponse) => {
const csrfToken = req.body.csrfToken;
const expectedToken = Cookies.get('csrf-token');
if (csrfToken !== expectedToken) {
return res.status(403).json({ error: 'Invalid CSRF token' });
}
// Handle the request
};
You can use a security scanner like SecuriSky to automatically detect and prevent CSRF attacks in your Next.js app.
Common CSRF Attack Vectors in Next.js Apps
CSRF attacks can be launched through various vectors, including:
* Image tags: An attacker can create an image tag that points to a vulnerable endpoint on your app.
* Form submissions: An attacker can create a form that submits a request to a vulnerable endpoint on your app.
* AJAX requests: An attacker can use JavaScript to send an AJAX request to a vulnerable endpoint on your app.
To protect against these attack vectors, you can use a combination of token-based validation and input validation. For example, you can validate user input to ensure that it conforms to expected formats and patterns:
const validateInput = (input: string) => {
const pattern = /^[a-zA-Z0-9]+$/;
if (!pattern.test(input)) {
throw new Error('Invalid input');
}
};
Quick Fix Checklist
Try it free
Scan your app for these issues now
Paste your URL and get a full security, performance, and SEO report in under 2 minutes — no signup required.
Run a free scan