From 2d80f3b045ea83ab5ed2057118e83fafc6d62e9f Mon Sep 17 00:00:00 2001 From: Erik Michelson Date: Mon, 13 Apr 2026 15:02:17 +0200 Subject: [PATCH] feat(csrf): add decorator to exclude routes from CSRF protection Signed-off-by: Erik Michelson --- backend/src/api/private/csrf/csrf.guard.ts | 14 ++++++++++++++ .../utils/decorators/csrf-exempt.decorator.ts | 16 ++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 backend/src/api/utils/decorators/csrf-exempt.decorator.ts diff --git a/backend/src/api/private/csrf/csrf.guard.ts b/backend/src/api/private/csrf/csrf.guard.ts index 66eb146a7..e3dd9d210 100644 --- a/backend/src/api/private/csrf/csrf.guard.ts +++ b/backend/src/api/private/csrf/csrf.guard.ts @@ -4,16 +4,30 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { CanActivate, ExecutionContext, Injectable, ForbiddenException } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; import type { FastifyRequest, FastifyReply } from 'fastify'; +import { CSRF_EXEMPT_KEY } from '../../utils/decorators/csrf-exempt.decorator'; + const UNPROTECTED_METHODS = ['GET', 'HEAD', 'OPTIONS']; @Injectable() export class CsrfGuard implements CanActivate { + constructor(private reflector: Reflector) {} + async canActivate(context: ExecutionContext): Promise { const request = context.switchToHttp().getRequest(); const reply = context.switchToHttp().getResponse(); + // Ignore if the @CsrfExempt() decorator is set for the route + const isCsrfExempt = this.reflector.getAllAndOverride(CSRF_EXEMPT_KEY, [ + context.getHandler(), + context.getClass(), + ]); + if (isCsrfExempt) { + return true; + } + // Ignore unprotected methods (GET, HEAD, OPTIONS) const method = request.method.toUpperCase(); if (UNPROTECTED_METHODS.includes(method)) { diff --git a/backend/src/api/utils/decorators/csrf-exempt.decorator.ts b/backend/src/api/utils/decorators/csrf-exempt.decorator.ts new file mode 100644 index 000000000..d154afe7c --- /dev/null +++ b/backend/src/api/utils/decorators/csrf-exempt.decorator.ts @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2026 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { SetMetadata } from '@nestjs/common'; + +export const CSRF_EXEMPT_KEY = 'csrf_exempt'; + +/** + * Decorator to mark a route as exempt from CSRF protection. + * Routes with this decorator won't be protected by CSRF protection. This is required for non-public-API endpoints + * called from non-browsers, e.g. OIDC backchannel-logout. + */ +export const CsrfExempt = () => SetMetadata(CSRF_EXEMPT_KEY, true);