All files / src/middleware csrf.js

0% Statements 0/19
0% Branches 0/10
0% Functions 0/2
0% Lines 0/19

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68                                                                                                                                       
/**
 * Middleware CSRF — Protection contre les requêtes cross-site forgées.
 *
 * Utilise le pattern "double submit cookie" :
 * 1. Le serveur génère un token aléatoire et l'envoie dans un cookie lisible par JS.
 * 2. Le frontend lit ce cookie et l'envoie dans le header X-CSRF-Token.
 * 3. Le serveur vérifie que le header correspond au cookie.
 *
 * Combiné avec SameSite=Lax sur les cookies d'auth, cela offre
 * une double protection contre les attaques CSRF.
 *
 * @module middleware/csrf
 */
 
const crypto = require('node:crypto');
 
const CSRF_COOKIE = 'csrf_token';
const CSRF_HEADER = 'x-csrf-token';
 
// Méthodes HTTP qui ne modifient pas l'état (pas besoin de CSRF)
const SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']);
 
/**
 * Middleware qui envoie un CSRF token en cookie si absent.
 * Doit être placé après cookie-parser.
 */
function csrfToken(req, res, next) {
  // Si pas de cookie CSRF, en générer un
  if (!req.cookies[CSRF_COOKIE]) {
    const token = crypto.randomBytes(32).toString('hex');
    const isProduction = process.env.NODE_ENV === 'production';
    res.cookie(CSRF_COOKIE, token, {
      httpOnly: false,  // Le JS doit pouvoir le lire
      secure: isProduction,
      sameSite: 'Lax',
      maxAge: 24 * 60 * 60 * 1000, // 24h
    });
  }
  next();
}
 
/**
 * Middleware qui valide le token CSRF pour les requêtes mutantes.
 * Vérifie que le header X-CSRF-Token correspond au cookie csrf_token.
 */
function csrfProtect(req, res, next) {
  // Les méthodes sûres n'ont pas besoin de CSRF
  if (SAFE_METHODS.has(req.method)) {
    return next();
  }
 
  const cookieToken = req.cookies[CSRF_COOKIE];
  const headerToken = req.headers[CSRF_HEADER];
 
  if (!cookieToken || !headerToken) {
    return res.status(403).json({ error: 'Token CSRF manquant.' });
  }
 
  // Comparaison en temps constant pour éviter les timing attacks
  if (!crypto.timingSafeEqual(Buffer.from(cookieToken), Buffer.from(headerToken))) {
    return res.status(403).json({ error: 'Token CSRF invalide.' });
  }
 
  next();
}
 
module.exports = { csrfToken, csrfProtect };