fix(webhook): match manual webhook repositories exactly

The manual webhook handlers selected target applications with a
`git_repository LIKE %full_name%` substring query, so a payload
repository name could match unintended applications when repository
names overlap.

Add a `MatchesManualWebhookApplications` trait that validates the
incoming `owner/repo` value and matches `Application.git_repository`
by exact normalized path. Github, Gitlab, Gitea and Bitbucket manual
handlers now use it, reject invalid repository input early, and return
a consistent generic webhook failure payload.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Andras Bacsai
2026-05-22 15:32:44 +02:00
co-authored by Claude Opus 4.7
parent 00ce43a9d0
commit c1518ba1c0
6 changed files with 351 additions and 60 deletions
+16 -13
View File
@@ -5,6 +5,7 @@ namespace App\Http\Controllers\Webhook;
use App\Actions\Application\CleanupPreviewDeployment;
use App\Http\Controllers\Controller;
use App\Http\Controllers\Webhook\Concerns\DetectsSkipDeployCommits;
use App\Http\Controllers\Webhook\Concerns\MatchesManualWebhookApplications;
use App\Models\Application;
use App\Models\ApplicationPreview;
use Exception;
@@ -15,6 +16,7 @@ use Visus\Cuid2\Cuid2;
class Gitlab extends Controller
{
use DetectsSkipDeployCommits;
use MatchesManualWebhookApplications;
public function manual(Request $request)
{
@@ -85,9 +87,18 @@ class Gitlab extends Controller
return response($return_payloads);
}
}
$applications = Application::where('git_repository', 'like', "%$full_name%");
$full_name = $this->manualWebhookRepositoryFullName($full_name);
if ($full_name === null) {
$return_payloads->push([
'status' => 'failed',
'message' => 'Nothing to do. Invalid repository.',
]);
return response($return_payloads);
}
$applications = Application::query();
if ($x_gitlab_event === 'push') {
$applications = $applications->where('git_branch', $branch)->get();
$applications = $this->manualWebhookApplications($applications->where('git_branch', $branch), $full_name);
if ($applications->isEmpty()) {
$return_payloads->push([
'status' => 'failed',
@@ -98,7 +109,7 @@ class Gitlab extends Controller
}
}
if ($x_gitlab_event === 'merge_request') {
$applications = $applications->where('git_branch', $base_branch)->get();
$applications = $this->manualWebhookApplications($applications->where('git_branch', $base_branch), $full_name);
if ($applications->isEmpty()) {
$return_payloads->push([
'status' => 'failed',
@@ -117,11 +128,7 @@ class Gitlab extends Controller
'repository' => $full_name ?? null,
'event' => $x_gitlab_event,
]);
$return_payloads->push([
'application' => $application->name,
'status' => 'failed',
'message' => 'Webhook secret not configured.',
]);
$return_payloads->push($this->unauthenticatedManualWebhookFailurePayload());
continue;
}
@@ -132,11 +139,7 @@ class Gitlab extends Controller
'repository' => $full_name ?? null,
'event' => $x_gitlab_event,
]);
$return_payloads->push([
'application' => $application->name,
'status' => 'failed',
'message' => 'Invalid signature.',
]);
$return_payloads->push($this->unauthenticatedManualWebhookFailurePayload());
continue;
}