diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index 2a64b3b21..0295aa5fc 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/bootstrap/helpers/api.php b/bootstrap/helpers/api.php index 7bda9c6a6..6a288a064 100644 --- a/bootstrap/helpers/api.php +++ b/bootstrap/helpers/api.php @@ -3,6 +3,7 @@ use App\Enums\BuildPackTypes; use App\Enums\RedirectTypes; use App\Enums\StaticImageTypes; +use App\Rules\ValidGitBranch; use App\Support\ValidationPatterns; use Illuminate\Database\Eloquent\Collection; use Illuminate\Http\Request; @@ -90,7 +91,7 @@ function sharedDataApplications() { return [ 'git_repository' => 'string', - 'git_branch' => 'string', + 'git_branch' => ['string', new ValidGitBranch], 'build_pack' => Rule::enum(BuildPackTypes::class), 'is_static' => 'boolean', 'is_spa' => 'boolean', diff --git a/tests/Feature/CommandInjectionSecurityTest.php b/tests/Feature/CommandInjectionSecurityTest.php index d42a8490a..558a8bd4f 100644 --- a/tests/Feature/CommandInjectionSecurityTest.php +++ b/tests/Feature/CommandInjectionSecurityTest.php @@ -1,6 +1,7 @@ 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', + ]); +});