fix(#278): Implement CSRF protection using double-submit cookie pattern
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed

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>
This commit is contained in:
2026-02-03 20:34:03 -06:00
parent 001a44532d
commit ebd842f007
8 changed files with 572 additions and 1 deletions

View File

@@ -12,6 +12,8 @@ import { FederationAuditService } from "./audit.service";
import { ConnectionService } from "./connection.service";
import { AuthGuard } from "../auth/guards/auth.guard";
import { AdminGuard } from "../auth/guards/admin.guard";
import { CsrfGuard } from "../common/guards/csrf.guard";
import { SkipCsrf } from "../common/decorators/skip-csrf.decorator";
import type { PublicInstanceIdentity } from "./types/instance.types";
import type { ConnectionDetails } from "./types/connection.types";
import type { AuthenticatedRequest } from "../common/types/user.types";
@@ -25,6 +27,7 @@ import {
import { FederationConnectionStatus } from "@prisma/client";
@Controller("api/v1/federation")
@UseGuards(CsrfGuard)
export class FederationController {
private readonly logger = new Logger(FederationController.name);
@@ -38,6 +41,7 @@ export class FederationController {
* Get this instance's public identity
* No authentication required - this is public information for federation
* Rate limit: "long" tier (200 req/hour) - public endpoint
* CSRF exempt: GET method (safe)
*/
@Get("instance")
@Throttle({ long: { limit: 200, ttl: 3600000 } })
@@ -207,8 +211,10 @@ export class FederationController {
* Handle incoming connection request from remote instance
* Public endpoint - no authentication required (signature-based verification)
* Rate limit: "short" tier (3 req/sec) - CRITICAL DoS protection (Issue #272)
* CSRF exempt: Uses signature-based authentication instead
*/
@Post("incoming/connect")
@SkipCsrf()
@Throttle({ short: { limit: 3, ttl: 1000 } })
async handleIncomingConnection(
@Body() dto: IncomingConnectionRequestDto