← 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 };