diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index 258b54eed..69240337f 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -5,6 +5,7 @@ use App\Actions\Application\GenerateConfig; use App\Jobs\ApplicationDeploymentJob; use App\Models\Application; +use App\Rules\ValidGitBranch; use App\Support\ValidationPatterns; use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; @@ -144,7 +145,7 @@ protected function rules(): array 'description' => ValidationPatterns::descriptionRules(), 'fqdn' => 'nullable', 'gitRepository' => 'required', - 'gitBranch' => 'required', + 'gitBranch' => ['required', 'string', new ValidGitBranch], 'gitCommitSha' => ['nullable', 'string', 'regex:/^[a-zA-Z0-9][a-zA-Z0-9._\-\/]*$/'], 'installCommand' => ValidationPatterns::shellSafeCommandRules(), 'buildCommand' => ValidationPatterns::shellSafeCommandRules(), diff --git a/app/Livewire/Project/Application/Source.php b/app/Livewire/Project/Application/Source.php index f14689ee0..3ee5919fe 100644 --- a/app/Livewire/Project/Application/Source.php +++ b/app/Livewire/Project/Application/Source.php @@ -6,6 +6,7 @@ use App\Models\GithubApp; use App\Models\GitlabApp; use App\Models\PrivateKey; +use App\Rules\ValidGitBranch; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Livewire\Attributes\Locked; use Livewire\Attributes\Validate; @@ -29,7 +30,7 @@ class Source extends Component #[Validate(['required', 'string'])] public string $gitRepository; - #[Validate(['required', 'string'])] + #[Validate(['required', 'string', new ValidGitBranch])] public string $gitBranch; #[Validate(['nullable', 'string', 'regex:/^[a-zA-Z0-9][a-zA-Z0-9._\-\/]*$/'])] diff --git a/tests/Feature/CommandInjectionSecurityTest.php b/tests/Feature/CommandInjectionSecurityTest.php index a450aa919..b327184a4 100644 --- a/tests/Feature/CommandInjectionSecurityTest.php +++ b/tests/Feature/CommandInjectionSecurityTest.php @@ -3,6 +3,7 @@ use App\Jobs\ApplicationDeploymentJob; use App\Models\Application; use App\Models\ApplicationSetting; +use App\Rules\ValidGitBranch; use App\Support\ValidationPatterns; describe('deployment job path field validation', function () { @@ -1074,3 +1075,46 @@ expect($merged['start_command'])->toContain('regex:'.ValidationPatterns::SHELL_SAFE_COMMAND_PATTERN); }); }); + +describe('git_branch validation rules survive array_merge in controller', function () { + test('git_branch uses ValidGitBranch in shared application rules', function () { + $rules = sharedDataApplications(); + + expect($rules['git_branch'])->toBeArray(); + expect(collect($rules['git_branch'])->contains(fn ($rule) => $rule instanceof ValidGitBranch))->toBeTrue(); + }); + + test('git_branch rejects shell metacharacter payloads', function (string $payload) { + $rules = sharedDataApplications(); + + $validator = validator( + ['git_branch' => $payload], + ['git_branch' => $rules['git_branch']] + ); + + expect($validator->fails())->toBeTrue(); + })->with([ + 'semicolon command separator' => 'main;touch /tmp/pwned;#', + 'command substitution' => 'main$(touch /tmp/pwned)', + 'backtick substitution' => 'main`touch /tmp/pwned`', + 'pipe operator' => 'main|id', + 'newline injection' => "main\ntouch /tmp/pwned", + 'redirect operator' => 'main>/tmp/pwned', + 'single quote breakout' => "main';id;#", + ]); + + test('git_branch accepts safe branch names', function (string $branch) { + $rules = sharedDataApplications(); + + $validator = validator( + ['git_branch' => $branch], + ['git_branch' => $rules['git_branch']] + ); + + expect($validator->fails())->toBeFalse(); + })->with([ + 'main', + 'feature/my-branch', + 'release_1.2.3', + ]); +});