Cross‑Site Request Forgery (CSRF) remains one of the most frequently exploited attack vectors in web applications. Although frameworks like Next.js provide many mechanisms that simplify building secure interfaces, developers often overlook basic protection measures. In this article we will show, step by step, how to secure a Next.js application against CSRF by combining SameSite cookie settings with the double‑submit token pattern. We will discuss the threat model, how it works, code examples, a mitigation checklist, and common pitfalls.
Threat model – what a CSRF attack looks like
A CSRF attack leverages the victim’s authorized browser context. The user is logged into the application and then visits a malicious site that sends an HTTP request to a protected endpoint (e.g., POST /api/transfer). The browser automatically includes session cookies, causing the server to treat the request as authentic. Without an additional verification mechanism, the server cannot distinguish this request from a legitimate user action.
Why SameSite cookies are not sufficient on their own
Setting the SameSite=Lax or SameSite=Strict flag on a session cookie limits its automatic transmission in cross‑site contexts. However, in practice there are scenarios where browsers still send the cookie (e.g., GET requests in Lax mode or unsupported older browsers). Therefore it is recommended to combine SameSite with an additional token that is verified on the server side.
Double‑submit token – how it works
The double‑submit token pattern involves generating a random token on the server and sending it both in a cookie and in a request header (or body). When the server receives a request, it compares the two values – if they match, the request is considered authentic. The token does not require server‑side storage (stateless), which simplifies scaling.
“The double token is the simplest way to introduce strong CSRF verification without adding server‑side session state.”
Implementation in Next.js – server side
Next.js provides API Routes that act as server‑side functions. Below is a code snippet that generates a token, stores it in a SameSite=Lax cookie, and validates it on subsequent requests.
import { NextApiRequest, NextApiResponse } from 'next';
import crypto from 'crypto';
const CSRF_COOKIE = 'csrfToken';
const CSRF_HEADER = 'x-csrf-token';
function generateToken() {
return crypto.randomBytes(32).toString('hex');
}
export default function handler(req: NextApiRequest, res: NextApiResponse) {
// 1️⃣ Generate token on GET (e.g., when rendering a form)
if (req.method === 'GET') {
const token = generateToken();
res.setHeader('Set-Cookie', `${CSRF_COOKIE}=${token}; HttpOnly; SameSite=Lax; Path=/; Secure`);
return res.status(200).json({ csrfToken: token });
}
// 2️⃣ Verify on POST/PUT/DELETE
const cookieToken = req.cookies[CSRF_COOKIE];
const headerToken = req.headers[CSRF_HEADER];
if (!cookieToken || !headerToken || cookieToken !== headerToken) {
return res.status(403).json({ error: 'Invalid CSRF token' });
}
// … further endpoint logic
return res.status(200).json({ success: true });
}
In this example the token is generated on the initial GET and returned both in a cookie and in the JSON response. The client (e.g., a React component) should read the token and include it in the x-csrf-token header on subsequent mutating requests.
Implementation in Next.js – client side (React)
In React components we can use the useEffect hook to fetch the token and set a default header in the axios library or fetch. Below is an example using fetch:
import { useEffect } from 'react';
export default function useCsrf() {
useEffect(() => {
fetch('/api/csrf')
.then(r => r.json())
.then(data => {
// token available in data.csrfToken and in the cookie
window.csrfToken = data.csrfToken;
});
}, []);
}
export async function csrfFetch(url, options = {}) {
const headers = {
...options.headers,
'x-csrf-token': window.csrfToken,
};
return fetch(url, { ...options, headers, credentials: 'include' });
}
Setting credentials: 'include' ensures the token cookie is sent with the request, while the x-csrf-token header enables double verification.
CSRF mitigation checklists in Next.js
- Set the
SameSite=Laxflag (orStrictif the application does not require cross‑site POST) andSecureon every session cookie. - Generate a unique token per session and store it in an http‑only cookie.
- Send the token in a dedicated header (
x-csrf-token) with all mutating requests. - Verify the cookie‑header token match on the server side before executing business logic.
- Limit the token’s lifetime (e.g., 30 min) and refresh it on each session.
- Ensure all API endpoints that accept data (POST, PUT, DELETE, PATCH) have CSRF verification enabled.
Typical mistakes and trade‑offs
Developers often make the following errors:
- Missing
SameSiteflag – in older browsers the token can be exploited, weakening protection. - Using a non‑httpOnly cookie – allows a malicious script to read the token, nullifying the benefit of double‑submit tokens.
- Storing the token in local storage – exposes it to theft via XSS.
- Checking the token only on the client side – does not provide real protection, as an attacker can bypass JavaScript code.
- Setting
SameSite=Strictin an app that requires external links – may cause unexpected rejection of legitimate requests.
In practice, the safest approach is to combine SameSite with a double‑submit token while also applying least‑privilege principles – minimize cookie and token permissions, restricting their scope to the necessary paths (Path=/api).
References to standards and best practices
The described techniques align with the OWASP Top 10 – A05:2021 – Security Misconfiguration guidelines and CWE‑352 (Cross‑Site Request Forgery). Additionally, using the SameSite flag and the least‑privilege rule is recommended in the OWASP Secure Headers Project. Implementing a double‑submit token satisfies OWASP ASVS 2.2.1 (CSRF Prevention).
Practical example – full API code and React hook
Below is a complete, copy‑ready example that can be placed in /pages/api/csrf.ts and in your own hook /hooks/useCsrf.ts. This gives you both an endpoint that generates a token and a simple function for making protected requests.
// /pages/api/csrf.ts
import { NextApiRequest, NextApiResponse } from 'next';
import crypto from 'crypto';
const COOKIE_NAME = 'csrfToken';
const HEADER_NAME = 'x-csrf-token';
function newToken() {
return crypto.randomBytes(24).toString('base64');
}
export default function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === 'GET') {
const token = newToken();
res.setHeader('Set-Cookie', `${COOKIE_NAME}=${token}; HttpOnly; SameSite=Lax; Secure; Path=/; Max-Age=1800`);
return res.status(200).json({ csrfToken: token });
}
const cookieToken = req.cookies[COOKIE_NAME];
const headerToken = req.headers[HEADER_NAME];
if (!cookieToken || !headerToken || cookieToken !== headerToken) {
return res.status(403).json({ error: 'Invalid CSRF token' });
}
// protected endpoint logic goes here
return res.status(200).json({ message: 'Protected action succeeded' });
}
// /hooks/useCsrf.ts
import { useEffect } from 'react';
export function useCsrf() {
useEffect(() => {
fetch('/api/csrf', { credentials: 'include' })
.then(r => r.json())
.then(data => {
(window as any).csrfToken = data.csrfToken;
});
}, []);
}
export async function csrfFetch(input: RequestInfo, init: RequestInit = {}) {
const token = (window as any).csrfToken;
const headers = new Headers(init.headers);
headers.set('x-csrf-token', token);
return fetch(input, { ...init, headers, credentials: 'include' });
}
After importing useCsrf in a component, the token will be fetched on mount, and every call to csrfFetch will automatically include the required header.
Summary and next steps
CSRF protection in Next.js applications does not require complex solutions – simply combine SameSite cookies, Secure flags, and the double‑submit token pattern. By following OWASP, CWE, and the least‑privilege principle, you provide a solid barrier against unauthorized requests. If you need a security audit, integration assistance, or want to elevate the protection level of your entire platform, contact Coderia.it – together we’ll build an application resilient to the latest threats.



