- Fix Next.js build failure: remove duplicate route at (dashboard)/listings/[id] that conflicted with (public)/listings/[id] (same URL path in two route groups) - Fix 772 ESLint errors: auto-fix import ordering (import-x/order), remove unused imports/variables, convert empty interfaces to type aliases, replace require() with ESM imports, fix consistent-type-imports violations - Add CLAUDE.md for developer onboarding documentation - All checks pass: 0 lint errors, typecheck clean, 230 tests passing, build success Co-Authored-By: Paperclip <noreply@paperclip.ing>
49 lines
1.5 KiB
TypeScript
49 lines
1.5 KiB
TypeScript
import { randomBytes } from 'node:crypto';
|
|
import { ForbiddenException, Injectable, type NestMiddleware } from '@nestjs/common';
|
|
import type { NextFunction, Request, Response } from 'express';
|
|
|
|
const CSRF_COOKIE = 'XSRF-TOKEN';
|
|
const CSRF_HEADER = 'x-csrf-token';
|
|
const TOKEN_LENGTH = 32;
|
|
|
|
const SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']);
|
|
|
|
@Injectable()
|
|
export class CsrfMiddleware implements NestMiddleware {
|
|
use(req: Request, res: Response, next: NextFunction): void {
|
|
// Safe methods: ensure a CSRF cookie exists for the client to read
|
|
if (SAFE_METHODS.has(req.method)) {
|
|
this.ensureCsrfCookie(req, res);
|
|
return next();
|
|
}
|
|
|
|
// State-changing methods: validate the double-submit token
|
|
const cookieToken = req.cookies?.[CSRF_COOKIE] as string | undefined;
|
|
const headerToken = req.headers[CSRF_HEADER] as string | undefined;
|
|
|
|
if (!cookieToken || !headerToken || cookieToken !== headerToken) {
|
|
throw new ForbiddenException('CSRF token missing or invalid');
|
|
}
|
|
|
|
// Rotate token after successful validation
|
|
this.setCsrfCookie(res);
|
|
next();
|
|
}
|
|
|
|
private ensureCsrfCookie(req: Request, res: Response): void {
|
|
if (!req.cookies?.[CSRF_COOKIE]) {
|
|
this.setCsrfCookie(res);
|
|
}
|
|
}
|
|
|
|
private setCsrfCookie(res: Response): void {
|
|
const token = randomBytes(TOKEN_LENGTH).toString('hex');
|
|
res.cookie(CSRF_COOKIE, token, {
|
|
httpOnly: false, // Frontend must read this cookie
|
|
secure: process.env['NODE_ENV'] === 'production',
|
|
sameSite: 'strict',
|
|
path: '/',
|
|
});
|
|
}
|
|
}
|