diff --git a/app/Http/Controllers/Webhook/Concerns/MatchesManualWebhookApplications.php b/app/Http/Controllers/Webhook/Concerns/MatchesManualWebhookApplications.php index 0463790eb..9fb3bb2de 100644 --- a/app/Http/Controllers/Webhook/Concerns/MatchesManualWebhookApplications.php +++ b/app/Http/Controllers/Webhook/Concerns/MatchesManualWebhookApplications.php @@ -79,9 +79,9 @@ protected function canonicalManualWebhookRepository(?string $gitRepository): ?st if (is_array($parts) && isset($parts['scheme'])) { $path = data_get($parts, 'path'); - } elseif (Str::startsWith($gitRepository, 'git@') && str_contains($gitRepository, ':')) { + } elseif (preg_match('/^[A-Za-z0-9._-]+@[^:]+:/', $gitRepository) === 1) { $path = Str::after($gitRepository, ':'); - // scp-style SSH URLs embed a custom port as "git@host:2222/owner/repo". + // scp-style SSH URLs embed a custom port as "user@host:2222/owner/repo". // Strip the leading numeric port segment so the path matches the webhook // payload's owner/repo, consistent with convertGitUrl() in shared.php. $path = preg_replace('#^\d+/#', '', $path) ?? $path; diff --git a/app/Livewire/Project/New/PublicGitRepository.php b/app/Livewire/Project/New/PublicGitRepository.php index 81d65bc85..1ef1855c1 100644 --- a/app/Livewire/Project/New/PublicGitRepository.php +++ b/app/Livewire/Project/New/PublicGitRepository.php @@ -137,10 +137,8 @@ public function loadBranch() throw new \RuntimeException('Invalid repository URL: '.$validator->errors()->first('repository_url')); } - if (str($this->repository_url)->startsWith('git@')) { - $github_instance = str($this->repository_url)->after('git@')->before(':'); - $repository = str($this->repository_url)->after(':')->before('.git'); - $this->repository_url = 'https://'.str($github_instance).'/'.$repository; + if (preg_match('/^(?[A-Za-z0-9._-]+)@(?[^:]+):(?.+)$/', $this->repository_url, $matches) === 1) { + $this->repository_url = 'https://'.$matches['host'].'/'.$matches['repository']; } if ( (str($this->repository_url)->startsWith('https://') || diff --git a/app/Rules/ValidGitRepositoryUrl.php b/app/Rules/ValidGitRepositoryUrl.php index ba1aed11b..7cccfa5e9 100644 --- a/app/Rules/ValidGitRepositoryUrl.php +++ b/app/Rules/ValidGitRepositoryUrl.php @@ -77,15 +77,15 @@ public function validate(string $attribute, mixed $value, Closure $fail): void } // Validate based on URL type - if (str_starts_with($value, 'git@')) { + if (preg_match('/^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+:/', $value)) { if (! $this->allowSSH) { $fail('SSH URLs are not allowed.'); return; } - // Validate SSH URL format (git@host:user/repo.git) - if (! preg_match('/^git@[a-zA-Z0-9\.\-]+:[a-zA-Z0-9\-_\/\.~]+$/', $value)) { + // Validate scp-style SSH URL format (user@host:user/repo.git) + if (! preg_match('/^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+:[a-zA-Z0-9\-_\/.~]+$/', $value)) { $fail('The :attribute is not a valid SSH repository URL.'); return; diff --git a/tests/Feature/Webhook/WebhookHmacTest.php b/tests/Feature/Webhook/WebhookHmacTest.php index 011b36731..45e0da377 100644 --- a/tests/Feature/Webhook/WebhookHmacTest.php +++ b/tests/Feature/Webhook/WebhookHmacTest.php @@ -542,6 +542,29 @@ function createApplicationWithWebhook(string $repo = 'test-org/test-repo', strin expect($response->getContent())->not->toContain('No applications found'); }); + test('github matches an ssh repository URL with a non-git username', function () { + $app = createApplicationWithWebhook(overrides: [ + 'git_repository' => 'custom-user@git.example.com:test-org/test-repo.git', + ]); + $secret = $app->manual_webhook_secret_github; + + $payload = json_encode([ + 'ref' => 'refs/heads/main', + 'repository' => ['full_name' => 'test-org/test-repo'], + 'after' => 'abc123', + 'commits' => [], + ]); + + $response = $this->call('POST', '/webhooks/source/github/events/manual', [], [], [], [ + 'HTTP_X-GitHub-Event' => 'push', + 'HTTP_X-Hub-Signature-256' => 'sha256='.hash_hmac('sha256', $payload, $secret), + 'CONTENT_TYPE' => 'application/json', + ], $payload); + + $response->assertOk(); + expect($response->getContent())->not->toContain('No applications found'); + }); + test('gitlab matches scp-style ssh repository URL with custom port', function () { $app = createApplicationWithWebhook(overrides: [ 'git_repository' => 'git@gitlab.example.com:2222/services/xyz.git', diff --git a/tests/Unit/ValidGitRepositoryUrlTest.php b/tests/Unit/ValidGitRepositoryUrlTest.php index da467dc4d..ee57657a0 100644 --- a/tests/Unit/ValidGitRepositoryUrlTest.php +++ b/tests/Unit/ValidGitRepositoryUrlTest.php @@ -107,6 +107,7 @@ 'git@github.com:user/repo.git', 'git@gitlab.com:user/repo.git', 'git@bitbucket.org:user/repo.git', + 'custom-user@git.example.com:organization/repository.git', ]; foreach ($validUrls as $url) { @@ -115,6 +116,14 @@ } }); +it('rejects non-SSH email-like repository URLs', function () { + $rule = new ValidGitRepositoryUrl; + + $validator = Validator::make(['url' => 'custom-user@git.example.com'], ['url' => $rule]); + + expect($validator->fails())->toBeTrue(); +}); + it('rejects SSH URLs when not allowed', function () { $rule = new ValidGitRepositoryUrl(allowSSH: false);