fix(validation): support scoped packages in file path validation (#8928)

This commit is contained in:
Andras Bacsai 2026-03-12 13:10:48 +01:00 committed by GitHub
commit 66840d64da
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 74 additions and 16 deletions

View file

@ -3929,7 +3929,7 @@ private function add_build_secrets_to_compose($composeFile)
private function validatePathField(string $value, string $fieldName): string private function validatePathField(string $value, string $fieldName): string
{ {
if (! preg_match('/^\/[a-zA-Z0-9._\-\/]+$/', $value)) { if (! preg_match(\App\Support\ValidationPatterns::FILE_PATH_PATTERN, $value)) {
throw new \RuntimeException("Invalid {$fieldName}: contains forbidden characters."); throw new \RuntimeException("Invalid {$fieldName}: contains forbidden characters.");
} }
if (str_contains($value, '..')) { if (str_contains($value, '..')) {

View file

@ -73,7 +73,7 @@ class General extends Component
#[Validate(['string', 'nullable'])] #[Validate(['string', 'nullable'])]
public ?string $dockerfile = null; public ?string $dockerfile = null;
#[Validate(['string', 'nullable', 'max:255', 'regex:/^\/[a-zA-Z0-9._\-\/]+$/'])] #[Validate(['string', 'nullable', 'max:255', 'regex:/^\/[a-zA-Z0-9._\-\/~@+]+$/'])]
public ?string $dockerfileLocation = null; public ?string $dockerfileLocation = null;
#[Validate(['string', 'nullable'])] #[Validate(['string', 'nullable'])]
@ -85,7 +85,7 @@ class General extends Component
#[Validate(['string', 'nullable'])] #[Validate(['string', 'nullable'])]
public ?string $dockerRegistryImageTag = null; public ?string $dockerRegistryImageTag = null;
#[Validate(['string', 'nullable', 'max:255', 'regex:/^\/[a-zA-Z0-9._\-\/]+$/'])] #[Validate(['string', 'nullable', 'max:255', 'regex:/^\/[a-zA-Z0-9._\-\/~@+]+$/'])]
public ?string $dockerComposeLocation = null; public ?string $dockerComposeLocation = null;
#[Validate(['string', 'nullable'])] #[Validate(['string', 'nullable'])]
@ -198,8 +198,8 @@ protected function rules(): array
'dockerfile' => 'nullable', 'dockerfile' => 'nullable',
'dockerRegistryImageName' => 'nullable', 'dockerRegistryImageName' => 'nullable',
'dockerRegistryImageTag' => 'nullable', 'dockerRegistryImageTag' => 'nullable',
'dockerfileLocation' => ['nullable', 'regex:/^\/[a-zA-Z0-9._\-\/]+$/'], 'dockerfileLocation' => ['nullable', 'regex:'.ValidationPatterns::FILE_PATH_PATTERN],
'dockerComposeLocation' => ['nullable', 'regex:/^\/[a-zA-Z0-9._\-\/]+$/'], 'dockerComposeLocation' => ['nullable', 'regex:'.ValidationPatterns::FILE_PATH_PATTERN],
'dockerCompose' => 'nullable', 'dockerCompose' => 'nullable',
'dockerComposeRaw' => 'nullable', 'dockerComposeRaw' => 'nullable',
'dockerfileTargetBuild' => 'nullable', 'dockerfileTargetBuild' => 'nullable',
@ -231,8 +231,8 @@ protected function messages(): array
return array_merge( return array_merge(
ValidationPatterns::combinedMessages(), ValidationPatterns::combinedMessages(),
[ [
'dockerfileLocation.regex' => 'The Dockerfile location must be a valid path starting with / and containing only alphanumeric characters, dots, hyphens, and slashes.', ...ValidationPatterns::filePathMessages('dockerfileLocation', 'Dockerfile'),
'dockerComposeLocation.regex' => 'The Docker Compose location must be a valid path starting with / and containing only alphanumeric characters, dots, hyphens, and slashes.', ...ValidationPatterns::filePathMessages('dockerComposeLocation', 'Docker Compose'),
'name.required' => 'The Name field is required.', 'name.required' => 'The Name field is required.',
'gitRepository.required' => 'The Git Repository field is required.', 'gitRepository.required' => 'The Git Repository field is required.',
'gitBranch.required' => 'The Git Branch field is required.', 'gitBranch.required' => 'The Git Branch field is required.',

View file

@ -168,7 +168,7 @@ public function submit()
'selected_repository_owner' => 'required|string|regex:/^[a-zA-Z0-9\-_]+$/', 'selected_repository_owner' => 'required|string|regex:/^[a-zA-Z0-9\-_]+$/',
'selected_repository_repo' => 'required|string|regex:/^[a-zA-Z0-9\-_\.]+$/', 'selected_repository_repo' => 'required|string|regex:/^[a-zA-Z0-9\-_\.]+$/',
'selected_branch_name' => ['required', 'string', new ValidGitBranch], 'selected_branch_name' => ['required', 'string', new ValidGitBranch],
'docker_compose_location' => ['nullable', 'string', 'max:255', 'regex:/^\/[a-zA-Z0-9._\-\/]+$/'], 'docker_compose_location' => \App\Support\ValidationPatterns::filePathRules(),
]); ]);
if ($validator->fails()) { if ($validator->fails()) {

View file

@ -64,7 +64,7 @@ class GithubPrivateRepositoryDeployKey extends Component
'is_static' => 'required|boolean', 'is_static' => 'required|boolean',
'publish_directory' => 'nullable|string', 'publish_directory' => 'nullable|string',
'build_pack' => 'required|string', 'build_pack' => 'required|string',
'docker_compose_location' => ['nullable', 'string', 'max:255', 'regex:/^\/[a-zA-Z0-9._\-\/]+$/'], 'docker_compose_location' => \App\Support\ValidationPatterns::filePathRules(),
]; ];
protected function rules() protected function rules()
@ -76,7 +76,7 @@ protected function rules()
'is_static' => 'required|boolean', 'is_static' => 'required|boolean',
'publish_directory' => 'nullable|string', 'publish_directory' => 'nullable|string',
'build_pack' => 'required|string', 'build_pack' => 'required|string',
'docker_compose_location' => ['nullable', 'string', 'max:255', 'regex:/^\/[a-zA-Z0-9._\-\/]+$/'], 'docker_compose_location' => \App\Support\ValidationPatterns::filePathRules(),
]; ];
} }

View file

@ -70,7 +70,7 @@ class PublicGitRepository extends Component
'publish_directory' => 'nullable|string', 'publish_directory' => 'nullable|string',
'build_pack' => 'required|string', 'build_pack' => 'required|string',
'base_directory' => 'nullable|string', 'base_directory' => 'nullable|string',
'docker_compose_location' => ['nullable', 'string', 'max:255', 'regex:/^\/[a-zA-Z0-9._\-\/]+$/'], 'docker_compose_location' => \App\Support\ValidationPatterns::filePathRules(),
]; ];
protected function rules() protected function rules()
@ -82,7 +82,7 @@ protected function rules()
'publish_directory' => 'nullable|string', 'publish_directory' => 'nullable|string',
'build_pack' => 'required|string', 'build_pack' => 'required|string',
'base_directory' => 'nullable|string', 'base_directory' => 'nullable|string',
'docker_compose_location' => ['nullable', 'string', 'max:255', 'regex:/^\/[a-zA-Z0-9._\-\/]+$/'], 'docker_compose_location' => \App\Support\ValidationPatterns::filePathRules(),
'git_branch' => ['required', 'string', new ValidGitBranch], 'git_branch' => ['required', 'string', new ValidGitBranch],
]; ];
} }

View file

@ -17,6 +17,12 @@ class ValidationPatterns
*/ */
public const DESCRIPTION_PATTERN = '/^[\p{L}\p{M}\p{N}\s\-_.,!?()\'\"+=*@\/&]+$/u'; public const DESCRIPTION_PATTERN = '/^[\p{L}\p{M}\p{N}\s\-_.,!?()\'\"+=*@\/&]+$/u';
/**
* Pattern for file paths (dockerfile location, docker compose location, etc.)
* Allows alphanumeric, dots, hyphens, underscores, slashes, @, ~, and +
*/
public const FILE_PATH_PATTERN = '/^\/[a-zA-Z0-9._\-\/~@+]+$/';
/** /**
* Get validation rules for name fields * Get validation rules for name fields
*/ */
@ -81,6 +87,24 @@ public static function descriptionMessages(): array
]; ];
} }
/**
* Get validation rules for file path fields (dockerfile location, docker compose location)
*/
public static function filePathRules(int $maxLength = 255): array
{
return ['nullable', 'string', 'max:'.$maxLength, 'regex:'.self::FILE_PATH_PATTERN];
}
/**
* Get validation messages for file path fields
*/
public static function filePathMessages(string $field = 'dockerfileLocation', string $label = 'Dockerfile'): array
{
return [
"{$field}.regex" => "The {$label} location must be a valid path starting with / and containing only alphanumeric characters, dots, hyphens, underscores, slashes, @, ~, and +.",
];
}
/** /**
* Get combined validation messages for both name and description fields * Get combined validation messages for both name and description fields
*/ */

View file

@ -134,8 +134,8 @@ function sharedDataApplications()
'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' => ['string', 'nullable', 'max:255', 'regex:/^\/[a-zA-Z0-9._\-\/]+$/'], 'dockerfile_location' => ['string', 'nullable', 'max:255', 'regex:'.\App\Support\ValidationPatterns::FILE_PATH_PATTERN],
'docker_compose_location' => ['string', 'nullable', 'max:255', 'regex:/^\/[a-zA-Z0-9._\-\/]+$/'], 'docker_compose_location' => ['string', 'nullable', 'max:255', 'regex:'.\App\Support\ValidationPatterns::FILE_PATH_PATTERN],
'docker_compose' => 'string|nullable', 'docker_compose' => 'string|nullable',
'docker_compose_domains' => 'array|nullable', 'docker_compose_domains' => 'array|nullable',
'docker_compose_custom_start_command' => 'string|nullable', 'docker_compose_custom_start_command' => 'string|nullable',

View file

@ -91,6 +91,28 @@
->toBe('/docker/Dockerfile.prod'); ->toBe('/docker/Dockerfile.prod');
}); });
test('allows path with @ symbol for scoped packages', function () {
$job = new ReflectionClass(ApplicationDeploymentJob::class);
$method = $job->getMethod('validatePathField');
$method->setAccessible(true);
$instance = $job->newInstanceWithoutConstructor();
expect($method->invoke($instance, '/packages/@intlayer/mcp/Dockerfile', 'dockerfile_location'))
->toBe('/packages/@intlayer/mcp/Dockerfile');
});
test('allows path with tilde and plus characters', function () {
$job = new ReflectionClass(ApplicationDeploymentJob::class);
$method = $job->getMethod('validatePathField');
$method->setAccessible(true);
$instance = $job->newInstanceWithoutConstructor();
expect($method->invoke($instance, '/build~v1/c++/Dockerfile', 'dockerfile_location'))
->toBe('/build~v1/c++/Dockerfile');
});
test('allows valid compose file path', function () { test('allows valid compose file path', function () {
$job = new ReflectionClass(ApplicationDeploymentJob::class); $job = new ReflectionClass(ApplicationDeploymentJob::class);
$method = $job->getMethod('validatePathField'); $method = $job->getMethod('validatePathField');
@ -149,6 +171,18 @@
}); });
}); });
test('dockerfile_location validation allows paths with @ for scoped packages', function () {
$rules = sharedDataApplications();
$validator = validator(
['dockerfile_location' => '/packages/@intlayer/mcp/Dockerfile'],
['dockerfile_location' => $rules['dockerfile_location']]
);
expect($validator->fails())->toBeFalse();
});
});
describe('sharedDataApplications rules survive array_merge in controller', function () { describe('sharedDataApplications rules survive array_merge in controller', function () {
test('docker_compose_location safe regex is not overridden by local rules', function () { test('docker_compose_location safe regex is not overridden by local rules', function () {
$sharedRules = sharedDataApplications(); $sharedRules = sharedDataApplications();
@ -164,7 +198,7 @@
// The merged rules for docker_compose_location should be the safe regex, not just 'string' // The merged rules for docker_compose_location should be the safe regex, not just 'string'
expect($merged['docker_compose_location'])->toBeArray(); expect($merged['docker_compose_location'])->toBeArray();
expect($merged['docker_compose_location'])->toContain('regex:/^\/[a-zA-Z0-9._\-\/]+$/'); expect($merged['docker_compose_location'])->toContain('regex:'.\App\Support\ValidationPatterns::FILE_PATH_PATTERN);
}); });
}); });