Files
stack/apps/api/src/common/guards/csrf.guard.ts
Jason Woltje ebd842f007
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
fix(#278): Implement CSRF protection using double-submit cookie pattern
Implemented comprehensive CSRF protection for all state-changing endpoints
(POST, PATCH, DELETE) using the double-submit cookie pattern.

Security Implementation:
- Created CsrfGuard using double-submit cookie validation
- Token set in httpOnly cookie and validated against X-CSRF-Token header
- Applied guard to FederationController (vulnerable endpoints)
- Safe HTTP methods (GET, HEAD, OPTIONS) automatically exempted
- Signature-based endpoints (@SkipCsrf decorator) exempted

Components Added:
- CsrfGuard: Validates cookie and header token match
- CsrfController: GET /api/v1/csrf/token endpoint for token generation
- @SkipCsrf(): Decorator to exempt endpoints with alternative auth
- Comprehensive tests (20 tests, all passing)

Protected Endpoints:
- POST /api/v1/federation/connections/initiate
- POST /api/v1/federation/connections/:id/accept
- POST /api/v1/federation/connections/:id/reject
- POST /api/v1/federation/connections/:id/disconnect
- POST /api/v1/federation/instance/regenerate-keys

Exempted Endpoints:
- POST /api/v1/federation/incoming/connect (signature-verified)
- GET requests (safe methods)

Security Features:
- httpOnly cookies prevent XSS attacks
- SameSite=strict prevents subdomain attacks
- Cryptographically secure random tokens (32 bytes)
- 24-hour token expiry
- Structured logging for security events

Testing:
- 14 guard tests covering all scenarios
- 6 controller tests for token generation
- Quality gates: lint, typecheck, build all passing

Note: Frontend integration required to use tokens. Clients must:
1. GET /api/v1/csrf/token to receive token
2. Include token in X-CSRF-Token header for state-changing requests

Fixes #278

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-03 20:35:00 -06:00

84 lines
2.2 KiB
TypeScript

/**
* 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<boolean>(SKIP_CSRF_KEY, [
context.getHandler(),
context.getClass(),
]);
if (skipCsrf) {
return true;
}
const request = context.switchToHttp().getRequest<Request>();
// 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<string, string> | 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;
}
}