From 7a626b0a82a58fac34a846927e4937db225d9035 Mon Sep 17 00:00:00 2001 From: Jennie Robinson Faber Date: Sun, 26 Apr 2026 13:16:11 +0100 Subject: [PATCH] fix(csrf): exempt /api/internal/ from double-submit check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reconcile-payments cron POSTs to /api/internal/reconcile-payments with an X-Reconcile-Token header but no csrf-token cookie/header. The CSRF middleware was 403ing the request before the route handler could check the shared secret — breaking Fix #6 (daily reconciliation cron). Found while wiring the Dokploy scheduled task. The Netlify scheduled function would have hit the same 403; nobody noticed because the site hasn't been deployed yet. Removing CSRF protection from /api/internal/ is safe: every route under that prefix is machine-to-machine and gates on its own shared-secret header. CSRF protects against browser-driven cross-origin POSTs, which isn't the threat model for these endpoints. Tests: 758 passing (CSRF middleware unit tests still cover the exempt list shape). --- server/middleware/01.csrf.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/middleware/01.csrf.js b/server/middleware/01.csrf.js index 3f3e841..b479c6e 100644 --- a/server/middleware/01.csrf.js +++ b/server/middleware/01.csrf.js @@ -2,11 +2,13 @@ import crypto from 'crypto' const SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']) -// Routes exempt from CSRF (external webhooks, magic link verify) +// Routes exempt from CSRF (external webhooks, magic link verify, machine-to- +// machine internal endpoints with their own shared-secret auth) const EXEMPT_PREFIXES = [ '/api/helcim/webhook', '/api/slack/webhook', '/api/auth/verify', + '/api/internal/', '/oidc/', ]