From e755f4c9e0080c25b99e850773b04f31544f35be Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 28 Sep 2026 10:13:20 +0200 Subject: [PATCH] fix(webhooks): always verify manual webhook signatures GitHub, Gitea and Bitbucket manual webhooks accepted any signature when APP_ENV=local. GitLab and the GitHub App webhook already did not. Co-Authored-By: Claude Opus 5.5 --- app/Http/Controllers/Webhook/Bitbucket.php | 2 +- app/Http/Controllers/Webhook/Gitea.php | 2 +- app/Http/Controllers/Webhook/Github.php | 2 +- tests/Feature/Webhook/WebhookHmacTest.php | 28 ++++++++++++++++++++++ 4 files changed, 31 insertions(+), 3 deletions(-) diff --git a/app/Http/Controllers/Webhook/Bitbucket.php b/app/Http/Controllers/Webhook/Bitbucket.php index 0ef1fbac85..e84d018ea7 100644 --- a/app/Http/Controllers/Webhook/Bitbucket.php +++ b/app/Http/Controllers/Webhook/Bitbucket.php @@ -120,7 +120,7 @@ class Bitbucket extends Controller } $hash = $parts[1]; $payloadHash = hash_hmac('sha256', $rawPayload, $webhook_secret); - if (! hash_equals($hash, $payloadHash) && ! isDev()) { + if (! hash_equals($hash, $payloadHash)) { auditLogWebhookFailure('bitbucket', 'invalid_signature', [ 'application_uuid' => $application->uuid, 'application_name' => $application->name, diff --git a/app/Http/Controllers/Webhook/Gitea.php b/app/Http/Controllers/Webhook/Gitea.php index 6fb2b39364..6ef1cfa644 100644 --- a/app/Http/Controllers/Webhook/Gitea.php +++ b/app/Http/Controllers/Webhook/Gitea.php @@ -106,7 +106,7 @@ class Gitea extends Controller continue; } $hmac = hash_hmac('sha256', $request->getContent(), $webhook_secret); - if (! hash_equals($x_hub_signature_256, $hmac) && ! isDev()) { + if (! hash_equals($x_hub_signature_256, $hmac)) { auditLogWebhookFailure('gitea', 'invalid_signature', [ 'application_uuid' => $application->uuid, 'application_name' => $application->name, diff --git a/app/Http/Controllers/Webhook/Github.php b/app/Http/Controllers/Webhook/Github.php index fb36ea3ab1..4e9ef7da65 100644 --- a/app/Http/Controllers/Webhook/Github.php +++ b/app/Http/Controllers/Webhook/Github.php @@ -119,7 +119,7 @@ class Github extends Controller continue; } $hmac = hash_hmac('sha256', $request->getContent(), $webhook_secret); - if (! hash_equals($x_hub_signature_256, $hmac) && ! isDev()) { + if (! hash_equals($x_hub_signature_256, $hmac)) { auditLogWebhookFailure('github', 'invalid_signature', [ 'application_uuid' => $application->uuid, 'application_name' => $application->name, diff --git a/tests/Feature/Webhook/WebhookHmacTest.php b/tests/Feature/Webhook/WebhookHmacTest.php index 588e4dccf0..be5109beaf 100644 --- a/tests/Feature/Webhook/WebhookHmacTest.php +++ b/tests/Feature/Webhook/WebhookHmacTest.php @@ -631,6 +631,34 @@ describe('Gitea Manual Webhook HMAC', function () { }); }); +describe('Manual Webhook HMAC in the local environment', function () { + test('rejects an invalid signature when APP_ENV is local', function (string $provider) { + config(['app.env' => 'local']); + Queue::fake(); + $application = makeWebhookApplicationServerFunctional(createApplicationWithWebhook()); + + $response = sendManualWebhookPush($this, $provider, $application, validSignature: false); + + $response->assertOk(); + expect($response->getContent())->toContain('Invalid signature') + ->not->toContain('Deployment queued'); + expect(ApplicationDeploymentQueue::query()->where('application_id', $application->id)->exists())->toBeFalse(); + })->with(['github', 'gitea', 'bitbucket']); + + test('accepts a valid signature when APP_ENV is local', function (string $provider) { + config(['app.env' => 'local']); + Queue::fake(); + $application = makeWebhookApplicationServerFunctional(createApplicationWithWebhook()); + + $response = sendManualWebhookPush($this, $provider, $application); + + $response->assertOk(); + expect($response->getContent())->toContain('Deployment queued') + ->not->toContain('Invalid signature'); + expect(ApplicationDeploymentQueue::query()->where('application_id', $application->id)->exists())->toBeTrue(); + })->with(['github', 'gitea', 'bitbucket']); +}); + describe('Manual Webhook Repository Matching', function () { test('github rejects empty repository without leaking applications', function () { $app = createApplicationWithWebhook(overrides: ['name' => 'secret-github-app']);