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 <noreply@anthropic.com>
This commit is contained in:
Andras Bacsai
2026-09-28 10:13:20 +02:00
co-authored by Claude Opus 5.5
parent 9c7f2dc412
commit e755f4c9e0
4 changed files with 31 additions and 3 deletions
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+28
View File
@@ -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']);