import { Request, Response, NextFunction } from 'express'; import { timingSafeEqual } from 'crypto'; import { verifyCsrfToken, isCsrfConfigured } from '../services/csrf.service'; import { env } from '../config/env'; import { logger } from '../lib/logger'; const SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']); /** * CSRF middleware с double-submit pattern. * * Требует ОБА source'а: cookie `csrf_token` AND header `X-CSRF-Token`, * сравнивает их constant-time. Без обоих или при несовпадении — 403. * * Защита: если attacker украл только cookie (auto-sent при cross-site POST), * он не может выставить header X-CSRF-Token из чужого origin без CORS, * а CORS у нас явный whitelist. Single-source check был bypass'able. */ export function csrfMiddleware(req: Request, res: Response, next: NextFunction): void { if (SAFE_METHODS.has(req.method)) { next(); return; } // CSRF полностью отключён через ENV (csrfPath пустой) — пропускаем явно. if (!env.vault.csrfPath) { next(); return; } // Bearer-auth (Authorization header, no access_token cookie) — CSRF не нужен. // Browser-based CSRF атаки требуют auto-sent cookie. Если auth идёт через explicit // Authorization header, attacker не может его выставить cross-origin (CORS preflight блокирует). // Это позволяет API-клиентам (Swagger UI, Postman, curl, мобилке) работать без CSRF. if (!req.cookies?.access_token && req.headers.authorization) { next(); return; } // CSRF включён, но секрет не загружен → fail-secure 503. if (!isCsrfConfigured()) { logger.error('CSRF check unavailable: secret not loaded — rejecting mutating request'); res.status(503).json({ success: false, error: 'CSRF protection unavailable, retry later' }); return; } const cookieToken = req.cookies?.csrf_token; const headerToken = req.headers['x-csrf-token']; // Double-submit: ОБА обязательны. if (!cookieToken || typeof cookieToken !== 'string' || !headerToken || typeof headerToken !== 'string') { res.status(403).json({ success: false, error: 'CSRF token missing (need cookie + header)' }); return; } // Constant-time сравнение cookie === header (защита от timing oracle). const a = Buffer.from(cookieToken); const b = Buffer.from(headerToken); if (a.length !== b.length || !timingSafeEqual(a, b)) { logger.warn('CSRF: cookie/header mismatch'); res.status(403).json({ success: false, error: 'CSRF token mismatch' }); return; } // HMAC verify только после совпадения двух source'ов. const result = verifyCsrfToken(cookieToken); if (!result.valid) { logger.warn(`CSRF validation failed: ${result.reason}`); res.status(403).json({ success: false, error: 'Invalid CSRF token' }); return; } next(); }