diff --git a/src/Process/HostProcessFactory.php b/src/Process/HostProcessFactory.php index 7271f27e..af7196d8 100644 --- a/src/Process/HostProcessFactory.php +++ b/src/Process/HostProcessFactory.php @@ -9,6 +9,11 @@ final class HostProcessFactory implements ProcessFactory { + /** + * Inherited env vars that are kept, everything else is unset so we don't leak secrets to the process + */ + private const ALLOWED_ENV = ['PATH', 'HOME', 'COMPOSER_HOME']; + private ExecutableFinder $executableFinder; public function __construct(?ExecutableFinder $executableFinder = null) @@ -28,19 +33,27 @@ public function create(ProcessInput $processInput): Process return new Process( [$executablePath, ...$processInput->getArgs()], $processInput->getWorkingDirectory(), - $this->getDefaultEnv() + $processInput->getEnv(), + array_merge($this->getDefaultEnv(), $processInput->getEnv()), $processInput->getInput(), 10, ); } /** - * @return array + * Symfony Process inherits the parent env, so we unset every inherited var by setting it to false. + * We read from getenv() rather than $_ENV, as $_ENV is empty when variables_order does not include E. + * + * @return array */ private function getDefaultEnv(): array { - $env = array_map(fn() => false, $_ENV); - $env + array_map(fn() => false, $_SERVER); + $env = []; + + foreach (array_keys(getenv() + $_ENV) as $name) { + if (!in_array($name, self::ALLOWED_ENV, true)) { + $env[(string) $name] = false; + } + } return $env; } diff --git a/test/Process/HostProcessFactoryTest.php b/test/Process/HostProcessFactoryTest.php index e2172e59..8e4c6cce 100644 --- a/test/Process/HostProcessFactoryTest.php +++ b/test/Process/HostProcessFactoryTest.php @@ -68,7 +68,23 @@ public function testCreateWithEnv(): void $input = new ProcessInput('composer', ['one', 'two'], __DIR__, ['SOME_VAR' => 'value']); $process = $factory->create($input); - static::assertSame(['SOME_VAR' => 'value'], $process->getEnv()); + static::assertSame('value', $process->getEnv()['SOME_VAR']); + } + + public function testInheritedEnvIsRemovedExceptAllowed(): void + { + putenv('PHPSCHOOL_SECRET=secret'); + + try { + $process = (new HostProcessFactory())->create( + new ProcessInput('php', ['-r', 'echo json_encode([getenv("PHPSCHOOL_SECRET"), getenv("PATH") !== false]);'], __DIR__, []), + ); + $process->mustRun(); + + static::assertSame('[false,true]', $process->getOutput()); + } finally { + putenv('PHPSCHOOL_SECRET'); + } } public function testWithInput(): void