From 601245d9692eee97d4c40e649040f67c40785cc8 Mon Sep 17 00:00:00 2001 From: Schwifty Date: Mon, 23 Mar 2026 01:43:43 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20harden=20auth=20middleware=20=E2=80=94?= =?UTF-8?q?=20exact=20route=20matching,=20remove=20admin=20bypass,=20add?= =?UTF-8?q?=20cron=20secret?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Switch str_contains() to exact match ($path === $route) in PUBLIC_ROUTES check to prevent substring-based route bypass attacks. 2. Remove blanket /api/admin/ bypass that was letting all admin endpoints through without authentication. 3. Add requireCronSecret() — cron/scheduled task endpoints now require a valid X-Cron-Secret header matching the PAYFRIT_CRON_SECRET env var. Uses hash_equals() for timing-safe comparison. Applied to: - cron/expireStaleChats.php - cron/expireTabs.php - api/admin/scheduledTasks/runDue.php --- api/admin/scheduledTasks/runDue.php | 2 +- api/helpers.php | 32 +++++++++++++++++++++++------ cron/expireStaleChats.php | 2 +- cron/expireTabs.php | 2 +- 4 files changed, 29 insertions(+), 9 deletions(-) diff --git a/api/admin/scheduledTasks/runDue.php b/api/admin/scheduledTasks/runDue.php index 7e183d1..5a249f0 100644 --- a/api/admin/scheduledTasks/runDue.php +++ b/api/admin/scheduledTasks/runDue.php @@ -1,7 +1,7 @@ false, 'message' => $errMsg]; } +// ============================================ +// CRON AUTH +// ============================================ + +/** + * Require a valid X-Cron-Secret header for cron/scheduled task endpoints. + * The secret is read from the PAYFRIT_CRON_SECRET environment variable. + * Aborts with 403 if missing or mismatched. + */ +function requireCronSecret(): void { + $expected = trim(getenv('PAYFRIT_CRON_SECRET') ?: ''); + if ($expected === '') { + error_log('[cron_auth] PAYFRIT_CRON_SECRET env var is not set. Blocking request.'); + http_response_code(403); + jsonResponse(['OK' => false, 'ERROR' => 'cron_secret_not_configured'], 403); + } + + $provided = headerValue('X-Cron-Secret'); + if ($provided === '' || !hash_equals($expected, $provided)) { + http_response_code(403); + jsonResponse(['OK' => false, 'ERROR' => 'invalid_cron_secret'], 403); + } +} + // ============================================ // AUTH MIDDLEWARE // ============================================ @@ -525,18 +549,14 @@ function runAuth(): void { $businessId = (int) $bizHeader; } - // Check if public route + // Check if public route (exact match only) $isPublic = false; foreach (PUBLIC_ROUTES as $route) { - if (str_contains($path, strtolower($route))) { + if ($path === strtolower($route)) { $isPublic = true; break; } } - // Also allow /api/admin/ paths - if (str_contains($path, '/api/admin/')) { - $isPublic = true; - } if (!$isPublic) { if ($userId <= 0) { diff --git a/cron/expireStaleChats.php b/cron/expireStaleChats.php index 46d8371..4a24420 100644 --- a/cron/expireStaleChats.php +++ b/cron/expireStaleChats.php @@ -1,6 +1,6 @@