/** * CSRF Guard * * Implements CSRF protection using double-submit cookie pattern. * Validates that CSRF token in cookie matches token in header. * * Usage: * - Apply to controllers handling state-changing operations * - Use @SkipCsrf() decorator to exempt specific endpoints * - Safe methods (GET, HEAD, OPTIONS) are automatically exempted */ import { Injectable, CanActivate, ExecutionContext, ForbiddenException, Logger, } from "@nestjs/common"; import { Reflector } from "@nestjs/core"; import { Request } from "express"; export const SKIP_CSRF_KEY = "skipCsrf"; @Injectable() export class CsrfGuard implements CanActivate { private readonly logger = new Logger(CsrfGuard.name); constructor(private reflector: Reflector) {} canActivate(context: ExecutionContext): boolean { // Check if endpoint is marked to skip CSRF const skipCsrf = this.reflector.getAllAndOverride(SKIP_CSRF_KEY, [ context.getHandler(), context.getClass(), ]); if (skipCsrf) { return true; } const request = context.switchToHttp().getRequest(); // Exempt safe HTTP methods (GET, HEAD, OPTIONS) if (["GET", "HEAD", "OPTIONS"].includes(request.method)) { return true; } // Get CSRF token from cookie and header const cookies = request.cookies as Record | undefined; const cookieToken = cookies?.["csrf-token"]; const headerToken = request.headers["x-csrf-token"] as string | undefined; // Validate tokens exist and match if (!cookieToken || !headerToken) { this.logger.warn({ event: "CSRF_TOKEN_MISSING", method: request.method, path: request.path, hasCookie: !!cookieToken, hasHeader: !!headerToken, securityEvent: true, timestamp: new Date().toISOString(), }); throw new ForbiddenException("CSRF token missing"); } if (cookieToken !== headerToken) { this.logger.warn({ event: "CSRF_TOKEN_MISMATCH", method: request.method, path: request.path, securityEvent: true, timestamp: new Date().toISOString(), }); throw new ForbiddenException("CSRF token mismatch"); } return true; } }