44 lines
1.3 KiB
JavaScript
44 lines
1.3 KiB
JavaScript
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
|
/**
|
|
* CSRF protection using the double-submit cookie pattern.
|
|
*
|
|
* On login the server issues a random `csrf_token` cookie (httpOnly:false
|
|
* so JS can read it). The SPA must send the same value in the
|
|
* `X-CSRF-Token` request header for every state-changing request (POST,
|
|
* PUT, PATCH, DELETE).
|
|
*
|
|
* Because the `sameSite: strict` session cookie already provides strong
|
|
* protection in modern browsers, this acts as defence-in-depth for
|
|
* older browsers and any edge cases.
|
|
*
|
|
* Safe methods (GET, HEAD, OPTIONS) are exempted.
|
|
*/
|
|
|
|
const SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']);
|
|
|
|
function verifyCsrf(req, res, next) {
|
|
if (SAFE_METHODS.has(req.method)) return next();
|
|
|
|
const cookieToken = req.cookies.csrf_token;
|
|
const headerToken = req.headers['x-csrf-token'];
|
|
|
|
if (!cookieToken || !headerToken) {
|
|
return res.status(403).json({ error: 'CSRF token missing' });
|
|
}
|
|
|
|
// Constant-time comparison to prevent timing attacks
|
|
if (cookieToken.length !== headerToken.length) {
|
|
return res.status(403).json({ error: 'CSRF token invalid' });
|
|
}
|
|
|
|
const a = Buffer.from(cookieToken);
|
|
const b = Buffer.from(headerToken);
|
|
if (!require('crypto').timingSafeEqual(a, b)) {
|
|
return res.status(403).json({ error: 'CSRF token invalid' });
|
|
}
|
|
|
|
next();
|
|
}
|
|
|
|
module.exports = verifyCsrf;
|