From a89f2b72d995ee4f1cdfe0f4a106f972fac7754a Mon Sep 17 00:00:00 2001 From: Chris Veleris Date: Mon, 13 Apr 2026 13:05:33 +0300 Subject: [PATCH] fix: exempt public unauthenticated endpoints from CSRF protection The lusca CSRF implementation was breaking login and registration because the frontend doesn't fetch or send CSRF tokens. This is a structural issue that requires frontend implementation. As a pragmatic fix, this commit exempts public unauthenticated endpoints from CSRF protection: - /api/login, /api/register, /api/verify-email - /api/version, /api/registration-status, /api/health - /api/oidc/* (all OIDC authentication endpoints) - /api/feature-flags Authenticated endpoints still require CSRF tokens via lusca. Also updates csrf.js to use lusca's token generation mechanism, making it compatible with the global lusca CSRF middleware. TODO: Implement proper CSRF token handling in the frontend for enhanced security on public endpoints. --- backend/app.js | 20 +++++++++++++++++++- backend/middleware/csrf.js | 34 +++++++++++++++++++++++++--------- 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/backend/app.js b/backend/app.js index 7ade690..a91bb9d 100644 --- a/backend/app.js +++ b/backend/app.js @@ -97,10 +97,28 @@ const lusca = require('lusca'); // Pre-check middleware to exempt test/Bearer requests before lusca runs app.use((req, res, next) => { + // Public unauthenticated endpoints that should bypass CSRF + // These are routes that don't require authentication and are used during login/registration + const publicPaths = [ + '/api/login', + '/api/register', + '/api/verify-email', + '/api/version', + '/api/registration-status', + '/api/health', + ]; + + const isPublicPath = publicPaths.some((path) => req.path === path); + const isOidcPath = req.path.startsWith('/api/oidc/'); + const isFeatureFlagsPath = req.path.startsWith('/api/feature-flags'); + // Mark exempt requests so lusca wrapper can skip them if ( process.env.NODE_ENV === 'test' || - req.headers.authorization?.startsWith('Bearer ') + req.headers.authorization?.startsWith('Bearer ') || + isPublicPath || + isOidcPath || + isFeatureFlagsPath ) { req._csrfExempt = true; } diff --git a/backend/middleware/csrf.js b/backend/middleware/csrf.js index 50227b6..90787c7 100644 --- a/backend/middleware/csrf.js +++ b/backend/middleware/csrf.js @@ -1,11 +1,16 @@ -const csurf = require('@dr.pogodin/csurf'); +const lusca = require('lusca'); -const csrfMiddleware = csurf({ - cookie: false, - value: (req) => { - return req.headers['x-csrf-token'] || req.body?._csrf; - }, -}); +const csrfMiddleware = (req, res, next) => { + if (!req.session) { + return res.status(500).json({ error: 'Session not initialized' }); + } + + if (!req.session._csrf) { + req.session._csrf = require('crypto').randomBytes(16).toString('hex'); + } + + next(); +}; const csrfProtection = (req, res, next) => { if ( @@ -16,11 +21,22 @@ const csrfProtection = (req, res, next) => { return next(); } - return csrfMiddleware(req, res, next); + return lusca.csrf({ + header: 'x-csrf-token', + cookie: false, + })(req, res, next); }; const generateToken = (req) => { - return req.csrfToken ? req.csrfToken() : ''; + if (!req.session) { + return ''; + } + + if (!req.session._csrf) { + req.session._csrf = require('crypto').randomBytes(16).toString('hex'); + } + + return req.session._csrf; }; module.exports = {