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 <[email protected]>
36 lines
1.0 KiB
TypeScript
36 lines
1.0 KiB
TypeScript
/**
|
|
* CSRF Controller
|
|
*
|
|
* Provides CSRF token generation endpoint for client applications.
|
|
*/
|
|
|
|
import { Controller, Get, Res } from "@nestjs/common";
|
|
import { Response } from "express";
|
|
import * as crypto from "crypto";
|
|
import { SkipCsrf } from "../decorators/skip-csrf.decorator";
|
|
|
|
@Controller("api/v1/csrf")
|
|
export class CsrfController {
|
|
/**
|
|
* Generate and set CSRF token
|
|
* Returns token to client and sets it in httpOnly cookie
|
|
*/
|
|
@Get("token")
|
|
@SkipCsrf() // This endpoint itself doesn't need CSRF protection
|
|
getCsrfToken(@Res({ passthrough: true }) response: Response): { token: string } {
|
|
// Generate cryptographically secure random token
|
|
const token = crypto.randomBytes(32).toString("hex");
|
|
|
|
// Set token in httpOnly cookie
|
|
response.cookie("csrf-token", token, {
|
|
httpOnly: true,
|
|
secure: process.env.NODE_ENV === "production",
|
|
sameSite: "strict",
|
|
maxAge: 24 * 60 * 60 * 1000, // 24 hours
|
|
});
|
|
|
|
// Return token to client (so it can include in X-CSRF-Token header)
|
|
return { token };
|
|
}
|
|
}
|