← back to Ventura Claw Leads
lib/csrf.js
45 lines
// CSRF — per-session token, double-submit pattern with timing-safe compare.
// Lifted from NPH. Skips GET/HEAD/OPTIONS, /webhooks/* (Stripe sig handles auth),
// and any req with `req.skipCsrf = true`.
const crypto = require('crypto');
function ensureToken(req) {
if (!req.session) return null;
if (!req.session.csrfToken) {
req.session.csrfToken = crypto.randomBytes(24).toString('base64url');
}
return req.session.csrfToken;
}
function csrfMiddleware(req, res, next) {
const token = ensureToken(req);
res.locals.csrfToken = token || '';
const safe = req.method === 'GET' || req.method === 'HEAD' || req.method === 'OPTIONS';
if (safe) return next();
if (req.path.startsWith('/webhooks/')) return next();
if (req.skipCsrf) return next();
const supplied = (req.body && req.body._csrf) || req.get('x-csrf-token') || '';
if (!token || !supplied) return reject(req, res);
let a, b;
try { a = Buffer.from(token); b = Buffer.from(supplied); }
catch { return reject(req, res); }
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return reject(req, res);
next();
}
function reject(req, res) {
if (req.accepts('json') && !req.accepts('html')) {
return res.status(403).json({ error: 'csrf_failed' });
}
return res.status(403).render('public/error', {
title: 'Request blocked',
message: 'Form expired or session lost — please reload the page and try again.'
});
}
module.exports = { csrfMiddleware, ensureToken };