fix: validate application branch updates

This commit is contained in:
Andras Bacsai 2026-06-01 15:13:04 +02:00
parent 4d0be415c8
commit 2bb07bbe9e
4 changed files with 64 additions and 16 deletions

View file

@ -5,6 +5,7 @@
use App\Actions\Application\GenerateConfig; use App\Actions\Application\GenerateConfig;
use App\Jobs\ApplicationDeploymentJob; use App\Jobs\ApplicationDeploymentJob;
use App\Models\Application; use App\Models\Application;
use App\Rules\ValidGitBranch;
use App\Support\ValidationPatterns; use App\Support\ValidationPatterns;
use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
@ -144,7 +145,7 @@ protected function rules(): array
'description' => ValidationPatterns::descriptionRules(), 'description' => ValidationPatterns::descriptionRules(),
'fqdn' => 'nullable', 'fqdn' => 'nullable',
'gitRepository' => 'required', 'gitRepository' => 'required',
'gitBranch' => 'required', 'gitBranch' => ['required', 'string', new ValidGitBranch],
'gitCommitSha' => ['nullable', 'string', 'regex:/^[a-zA-Z0-9][a-zA-Z0-9._\-\/]*$/'], 'gitCommitSha' => ['nullable', 'string', 'regex:/^[a-zA-Z0-9][a-zA-Z0-9._\-\/]*$/'],
'installCommand' => ValidationPatterns::shellSafeCommandRules(), 'installCommand' => ValidationPatterns::shellSafeCommandRules(),
'buildCommand' => ValidationPatterns::shellSafeCommandRules(), 'buildCommand' => ValidationPatterns::shellSafeCommandRules(),

View file

@ -6,6 +6,7 @@
use App\Models\GithubApp; use App\Models\GithubApp;
use App\Models\GitlabApp; use App\Models\GitlabApp;
use App\Models\PrivateKey; use App\Models\PrivateKey;
use App\Rules\ValidGitBranch;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Attributes\Locked; use Livewire\Attributes\Locked;
use Livewire\Attributes\Validate; use Livewire\Attributes\Validate;
@ -29,7 +30,7 @@ class Source extends Component
#[Validate(['required', 'string'])] #[Validate(['required', 'string'])]
public string $gitRepository; public string $gitRepository;
#[Validate(['required', 'string'])] #[Validate(['required', 'string', new ValidGitBranch])]
public string $gitBranch; public string $gitBranch;
#[Validate(['nullable', 'string', 'regex:/^[a-zA-Z0-9][a-zA-Z0-9._\-\/]*$/'])] #[Validate(['nullable', 'string', 'regex:/^[a-zA-Z0-9][a-zA-Z0-9._\-\/]*$/'])]

View file

@ -3,6 +3,8 @@
use App\Enums\BuildPackTypes; use App\Enums\BuildPackTypes;
use App\Enums\RedirectTypes; use App\Enums\RedirectTypes;
use App\Enums\StaticImageTypes; use App\Enums\StaticImageTypes;
use App\Rules\ValidGitBranch;
use App\Support\ValidationPatterns;
use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
@ -83,7 +85,7 @@ function sharedDataApplications()
{ {
return [ return [
'git_repository' => 'string', 'git_repository' => 'string',
'git_branch' => 'string', 'git_branch' => ['string', new ValidGitBranch],
'build_pack' => Rule::enum(BuildPackTypes::class), 'build_pack' => Rule::enum(BuildPackTypes::class),
'is_static' => 'boolean', 'is_static' => 'boolean',
'is_spa' => 'boolean', 'is_spa' => 'boolean',
@ -95,14 +97,14 @@ function sharedDataApplications()
'git_commit_sha' => ['string', 'regex:/^[a-zA-Z0-9][a-zA-Z0-9._\-\/]*$/'], 'git_commit_sha' => ['string', 'regex:/^[a-zA-Z0-9][a-zA-Z0-9._\-\/]*$/'],
'docker_registry_image_name' => 'string|nullable', 'docker_registry_image_name' => 'string|nullable',
'docker_registry_image_tag' => 'string|nullable', 'docker_registry_image_tag' => 'string|nullable',
'install_command' => \App\Support\ValidationPatterns::shellSafeCommandRules(), 'install_command' => ValidationPatterns::shellSafeCommandRules(),
'build_command' => \App\Support\ValidationPatterns::shellSafeCommandRules(), 'build_command' => ValidationPatterns::shellSafeCommandRules(),
'start_command' => \App\Support\ValidationPatterns::shellSafeCommandRules(), 'start_command' => ValidationPatterns::shellSafeCommandRules(),
'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/', 'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/',
'ports_mappings' => 'string|regex:/^(\d+:\d+)(,\d+:\d+)*$/|nullable', 'ports_mappings' => 'string|regex:/^(\d+:\d+)(,\d+:\d+)*$/|nullable',
'custom_network_aliases' => 'string|nullable', 'custom_network_aliases' => 'string|nullable',
'base_directory' => \App\Support\ValidationPatterns::directoryPathRules(), 'base_directory' => ValidationPatterns::directoryPathRules(),
'publish_directory' => \App\Support\ValidationPatterns::directoryPathRules(), 'publish_directory' => ValidationPatterns::directoryPathRules(),
'health_check_enabled' => 'boolean', 'health_check_enabled' => 'boolean',
'health_check_type' => 'string|in:http,cmd', 'health_check_type' => 'string|in:http,cmd',
'health_check_command' => ['nullable', 'string', 'max:1000', 'regex:/^[a-zA-Z0-9 \-_.\/:=@,+]+$/'], 'health_check_command' => ['nullable', 'string', 'max:1000', 'regex:/^[a-zA-Z0-9 \-_.\/:=@,+]+$/'],
@ -125,24 +127,24 @@ function sharedDataApplications()
'limits_cpuset' => 'string|nullable', 'limits_cpuset' => 'string|nullable',
'limits_cpu_shares' => 'numeric', 'limits_cpu_shares' => 'numeric',
'custom_labels' => 'string|nullable', 'custom_labels' => 'string|nullable',
'custom_docker_run_options' => \App\Support\ValidationPatterns::shellSafeCommandRules(2000), 'custom_docker_run_options' => ValidationPatterns::shellSafeCommandRules(2000),
// Security: deployment commands are intentionally arbitrary shell (e.g. "php artisan migrate"). // Security: deployment commands are intentionally arbitrary shell (e.g. "php artisan migrate").
// Access is gated by API token authentication. Commands run inside the app container, not the host. // Access is gated by API token authentication. Commands run inside the app container, not the host.
'post_deployment_command' => 'string|nullable', 'post_deployment_command' => 'string|nullable',
'post_deployment_command_container' => \App\Support\ValidationPatterns::containerNameRules(), 'post_deployment_command_container' => ValidationPatterns::containerNameRules(),
'pre_deployment_command' => 'string|nullable', 'pre_deployment_command' => 'string|nullable',
'pre_deployment_command_container' => \App\Support\ValidationPatterns::containerNameRules(), 'pre_deployment_command_container' => ValidationPatterns::containerNameRules(),
'manual_webhook_secret_github' => 'string|nullable', 'manual_webhook_secret_github' => 'string|nullable',
'manual_webhook_secret_gitlab' => 'string|nullable', 'manual_webhook_secret_gitlab' => 'string|nullable',
'manual_webhook_secret_bitbucket' => 'string|nullable', 'manual_webhook_secret_bitbucket' => 'string|nullable',
'manual_webhook_secret_gitea' => 'string|nullable', 'manual_webhook_secret_gitea' => 'string|nullable',
'dockerfile_location' => \App\Support\ValidationPatterns::filePathRules(), 'dockerfile_location' => ValidationPatterns::filePathRules(),
'dockerfile_target_build' => \App\Support\ValidationPatterns::dockerTargetRules(), 'dockerfile_target_build' => ValidationPatterns::dockerTargetRules(),
'docker_compose_location' => \App\Support\ValidationPatterns::filePathRules(), 'docker_compose_location' => ValidationPatterns::filePathRules(),
'docker_compose' => 'string|nullable', 'docker_compose' => 'string|nullable',
'docker_compose_domains' => 'array|nullable', 'docker_compose_domains' => 'array|nullable',
'docker_compose_custom_start_command' => \App\Support\ValidationPatterns::shellSafeCommandRules(), 'docker_compose_custom_start_command' => ValidationPatterns::shellSafeCommandRules(),
'docker_compose_custom_build_command' => \App\Support\ValidationPatterns::shellSafeCommandRules(), 'docker_compose_custom_build_command' => ValidationPatterns::shellSafeCommandRules(),
'is_container_label_escape_enabled' => 'boolean', 'is_container_label_escape_enabled' => 'boolean',
'is_preserve_repository_enabled' => 'boolean', 'is_preserve_repository_enabled' => 'boolean',
]; ];

View file

@ -1,6 +1,7 @@
<?php <?php
use App\Jobs\ApplicationDeploymentJob; use App\Jobs\ApplicationDeploymentJob;
use App\Rules\ValidGitBranch;
use App\Support\ValidationPatterns; use App\Support\ValidationPatterns;
describe('deployment job path field validation', function () { describe('deployment job path field validation', function () {
@ -978,3 +979,46 @@
expect($merged['start_command'])->toContain('regex:'.ValidationPatterns::SHELL_SAFE_COMMAND_PATTERN); 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',
]);
});