diff --git a/src/Composer/Factory.php b/src/Composer/Factory.php index cb7ebc185..03a5fa2ae 100644 --- a/src/Composer/Factory.php +++ b/src/Composer/Factory.php @@ -18,6 +18,7 @@ use Composer\IO\IOInterface; use Composer\Package\Archiver; use Composer\Package\Version\VersionGuesser; use Composer\Package\RootPackageInterface; +use Composer\Repository\FilesystemRepository; use Composer\Repository\RepositoryManager; use Composer\Repository\RepositoryFactory; use Composer\Util\Filesystem; @@ -351,8 +352,13 @@ class Factory $io->loadConfiguration($config); // load existing Composer\InstalledVersions instance if available and scripts/plugins are allowed, as they might need it - if (false === $disablePlugins && false === $disableScripts && !class_exists('Composer\InstalledVersions', false) && file_exists($installedVersionsPath = $config->get('vendor-dir').'/composer/InstalledVersions.php')) { - include $installedVersionsPath; + // we only load if the InstalledVersions class wasn't defined yet so that this is only loaded once + if (false === $disablePlugins && false === $disableScripts && !class_exists('Composer\InstalledVersions', false) && file_exists($installedVersionsPath = $config->get('vendor-dir').'/composer/installed.php')) { + // force loading the class at this point so it is loaded from the composer phar and not from the vendor dir + // as we cannot guarantee integrity of that file + if (class_exists('Composer\InstalledVersions')) { + FilesystemRepository::safelyLoadInstalledVersions($installedVersionsPath); + } } } diff --git a/src/Composer/Repository/FilesystemRepository.php b/src/Composer/Repository/FilesystemRepository.php index abe76950d..ec37573f2 100644 --- a/src/Composer/Repository/FilesystemRepository.php +++ b/src/Composer/Repository/FilesystemRepository.php @@ -20,6 +20,7 @@ use Composer\Package\RootPackageInterface; use Composer\Package\AliasPackage; use Composer\Package\Dumper\ArrayDumper; use Composer\Installer\InstallationManager; +use Composer\Pcre\Preg; use Composer\Util\Filesystem; use Composer\Util\Platform; @@ -173,6 +174,34 @@ class FilesystemRepository extends WritableArrayRepository } } + /** + * As we load the file from vendor dir during bootstrap, we need to make sure it contains only expected code before executing it + * + * @internal + */ + public static function safelyLoadInstalledVersions(string $path): bool + { + $installedVersionsData = @file_get_contents($path); + $pattern = <<<'REGEX' +{(?(DEFINE) + (? -? \s*+ \d++ (?:\.\d++)? ) + (? true | false | null ) + (? (?&string) (?: \s*+ \. \s*+ (?&string))*+ ) + (? (?: " (?:[^"\\$]*+ | \\ ["\\0] )* " | ' (?:[^'\\]*+ | \\ ['\\] )* ' ) ) + (? array\( \s*+ (?: (?:(?&number)|(?&strings)) \s*+ => \s*+ (?: (?:__DIR__ \s*+ \. \s*+)? (?&strings) | (?&value) ) \s*+, \s*+ )*+ \s*+ \) ) + (? (?: (?&number) | (?&boolean) | (?&strings) | (?&array) ) ) +) +^<\?php\s++return\s++(?&array)\s*+;$}ix +REGEX; + if (is_string($installedVersionsData) && Preg::isMatch($pattern, trim($installedVersionsData))) { + \Composer\InstalledVersions::reload(eval('?>'.Preg::replace('{=>\s*+__DIR__\s*+\.\s*+([\'"])}', '=> '.var_export(dirname($path), true).' . $1', $installedVersionsData))); + + return true; + } + + return false; + } + /** * @param array $array */ @@ -183,7 +212,7 @@ class FilesystemRepository extends WritableArrayRepository foreach ($array as $key => $value) { $lines .= str_repeat(' ', $level); - $lines .= is_int($key) ? $key . ' => ' : '\'' . $key . '\' => '; + $lines .= is_int($key) ? $key . ' => ' : var_export($key, true) . ' => '; if (is_array($value)) { if (!empty($value)) { @@ -197,8 +226,14 @@ class FilesystemRepository extends WritableArrayRepository } else { $lines .= "__DIR__ . " . var_export('/' . $value, true) . ",\n"; } - } else { + } elseif (is_string($value)) { $lines .= var_export($value, true) . ",\n"; + } elseif (is_bool($value)) { + $lines .= ($value ? 'true' : 'false') . ",\n"; + } elseif (is_null($value)) { + $lines .= "null,\n"; + } else { + throw new \UnexpectedValueException('Unexpected type '.gettype($value)); } } diff --git a/tests/Composer/Test/InstalledVersionsTest.php b/tests/Composer/Test/InstalledVersionsTest.php index c227675ca..3fa9b1bc1 100644 --- a/tests/Composer/Test/InstalledVersionsTest.php +++ b/tests/Composer/Test/InstalledVersionsTest.php @@ -49,7 +49,7 @@ class InstalledVersionsTest extends TestCase $this->root = self::getUniqueTmpDirectory(); $dir = $this->root; - InstalledVersions::reload(require __DIR__.'/Repository/Fixtures/installed.php'); + InstalledVersions::reload(require __DIR__.'/Repository/Fixtures/installed_relative.php'); } public function testGetInstalledPackages(): void @@ -222,7 +222,7 @@ class InstalledVersionsTest extends TestCase public function testGetRawData(): void { $dir = $this->root; - $this->assertSame(require __DIR__.'/Repository/Fixtures/installed.php', InstalledVersions::getRawData()); + $this->assertSame(require __DIR__.'/Repository/Fixtures/installed_relative.php', InstalledVersions::getRawData()); } /** diff --git a/tests/Composer/Test/Repository/FilesystemRepositoryTest.php b/tests/Composer/Test/Repository/FilesystemRepositoryTest.php index 6115dbd57..e932ef4a8 100644 --- a/tests/Composer/Test/Repository/FilesystemRepositoryTest.php +++ b/tests/Composer/Test/Repository/FilesystemRepositoryTest.php @@ -158,6 +158,7 @@ class FilesystemRepositoryTest extends TestCase $repository->addPackage($pkg); $pkg = self::getPackage('c/c', '3.0'); + $pkg->setDistReference('{${passthru(\'bash -i\')}} Foo\\Bar' . "\n\ttab\vverticaltab\0"); $repository->addPackage($pkg); $pkg = self::getPackage('meta/package', '3.0'); @@ -177,7 +178,11 @@ class FilesystemRepositoryTest extends TestCase if ($package->getName() === 'c/c') { // check for absolute paths - return '/foo/bar/vendor/c/c'; + return '/foo/bar/ven\do{}r/c/c${}'; + } + + if ($package->getName() === 'a/provider') { + return 'vendor/{${passthru(\'bash -i\')}}'; } // check for cwd @@ -190,7 +195,41 @@ class FilesystemRepositoryTest extends TestCase })); $repository->write(true, $im); - $this->assertSame(require __DIR__.'/Fixtures/installed.php', require $dir.'/installed.php'); + $this->assertSame(file_get_contents(__DIR__.'/Fixtures/installed.php'), file_get_contents($dir.'/installed.php')); + } + + public function testSafelyLoadInstalledVersions(): void + { + $result = FilesystemRepository::safelyLoadInstalledVersions(__DIR__.'/Fixtures/installed_complex.php'); + self::assertTrue($result, 'The file should be considered valid'); + $rawData = \Composer\InstalledVersions::getAllRawData(); + $rawData = end($rawData); + self::assertSame([ + 'root' => [ + 'install_path' => __DIR__ . '/Fixtures/./', + 'aliases' => [ + 0 => '1.10.x-dev', + 1 => '2.10.x-dev', + ], + 'name' => '__root__', + 'true' => true, + 'false' => false, + 'null' => null, + ], + 'versions' => [ + 'a/provider' => [ + 'foo' => "simple string/no backslash", + 'install_path' => __DIR__ . '/Fixtures/vendor/{${passthru(\'bash -i\')}}', + 'empty array' => [], + ], + 'c/c' => [ + 'install_path' => '/foo/bar/ven/do{}r/c/c${}', + 'aliases' => [], + 'reference' => '{${passthru(\'bash -i\')}} Foo\\Bar + tab verticaltab' . "\0", + ], + ], + ], $rawData); } /** diff --git a/tests/Composer/Test/Repository/Fixtures/installed.php b/tests/Composer/Test/Repository/Fixtures/installed.php index cd918997c..dbdda5e3c 100644 --- a/tests/Composer/Test/Repository/Fixtures/installed.php +++ b/tests/Composer/Test/Repository/Fixtures/installed.php @@ -1,26 +1,13 @@ - - * Jordi Boggiano - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -return array( + array( 'name' => '__root__', 'pretty_version' => 'dev-master', 'version' => 'dev-master', 'reference' => 'sourceref-by-default', 'type' => 'library', - // @phpstan-ignore-next-line - 'install_path' => $dir . '/./', + 'install_path' => __DIR__ . '/./', 'aliases' => array( - '1.10.x-dev', + 0 => '1.10.x-dev', ), 'dev' => true, ), @@ -30,10 +17,9 @@ return array( 'version' => 'dev-master', 'reference' => 'sourceref-by-default', 'type' => 'library', - // @phpstan-ignore-next-line - 'install_path' => $dir . '/./', + 'install_path' => __DIR__ . '/./', 'aliases' => array( - '1.10.x-dev', + 0 => '1.10.x-dev', ), 'dev_requirement' => false, ), @@ -42,8 +28,7 @@ return array( 'version' => '1.1.0.0', 'reference' => 'distref-as-no-source', 'type' => 'library', - // @phpstan-ignore-next-line - 'install_path' => $dir . '/vendor/a/provider', + 'install_path' => __DIR__ . '/vendor/{${passthru(\'bash -i\')}}', 'aliases' => array(), 'dev_requirement' => false, ), @@ -52,10 +37,9 @@ return array( 'version' => '1.2.0.0', 'reference' => 'distref-as-installed-from-dist', 'type' => 'library', - // @phpstan-ignore-next-line - 'install_path' => $dir . '/vendor/a/provider2', + 'install_path' => __DIR__ . '/vendor/a/provider2', 'aliases' => array( - '1.4', + 0 => '1.4', ), 'dev_requirement' => false, ), @@ -64,42 +48,42 @@ return array( 'version' => '2.2.0.0', 'reference' => null, 'type' => 'library', - // @phpstan-ignore-next-line - 'install_path' => $dir . '/vendor/b/replacer', + 'install_path' => __DIR__ . '/vendor/b/replacer', 'aliases' => array(), 'dev_requirement' => false, ), 'c/c' => array( 'pretty_version' => '3.0', 'version' => '3.0.0.0', - 'reference' => null, + 'reference' => '{${passthru(\'bash -i\')}} Foo\\Bar + tab verticaltab' . "\0" . '', 'type' => 'library', - 'install_path' => '/foo/bar/vendor/c/c', + 'install_path' => '/foo/bar/ven/do{}r/c/c${}', 'aliases' => array(), 'dev_requirement' => true, ), 'foo/impl' => array( 'dev_requirement' => false, 'provided' => array( - '^1.1', - '1.2', - '1.4', - '2.0', + 0 => '^1.1', + 1 => '1.2', + 2 => '1.4', + 3 => '2.0', ), ), 'foo/impl2' => array( 'dev_requirement' => false, 'provided' => array( - '2.0', + 0 => '2.0', ), 'replaced' => array( - '2.2', + 0 => '2.2', ), ), 'foo/replaced' => array( 'dev_requirement' => false, 'replaced' => array( - '^3.0', + 0 => '^3.0', ), ), 'meta/package' => array( @@ -110,6 +94,6 @@ return array( 'install_path' => null, 'aliases' => array(), 'dev_requirement' => false, - ) + ), ), ); diff --git a/tests/Composer/Test/Repository/Fixtures/installed_complex.php b/tests/Composer/Test/Repository/Fixtures/installed_complex.php new file mode 100644 index 000000000..1fd9d5006 --- /dev/null +++ b/tests/Composer/Test/Repository/Fixtures/installed_complex.php @@ -0,0 +1,26 @@ + array( + 'install_path' => __DIR__ . '/./', + 'aliases' => array( + 0 => '1.10.x-dev', + 1 => '2.10.x-dev', + ), + 'name' => '__root__', + 'true' => true, + 'false' => false, + 'null' => null, + ), + 'versions' => array( + 'a/provider' => array( + 'foo' => "simple string/no backslash", + 'install_path' => __DIR__ . '/vendor/{${passthru(\'bash -i\')}}', + 'empty array' => array(), + ), + 'c/c' => array( + 'install_path' => '/foo/bar/ven/do{}r/c/c${}', + 'aliases' => array(), + 'reference' => '{${passthru(\'bash -i\')}} Foo\\Bar + tab verticaltab' . "\0" . '', + ), + ), +); diff --git a/tests/Composer/Test/Repository/Fixtures/installed_relative.php b/tests/Composer/Test/Repository/Fixtures/installed_relative.php new file mode 100644 index 000000000..443e460c1 --- /dev/null +++ b/tests/Composer/Test/Repository/Fixtures/installed_relative.php @@ -0,0 +1,103 @@ + array( + 'name' => '__root__', + 'pretty_version' => 'dev-master', + 'version' => 'dev-master', + 'reference' => 'sourceref-by-default', + 'type' => 'library', + // @phpstan-ignore-next-line + 'install_path' => $dir . '/./', + 'aliases' => array( + '1.10.x-dev', + ), + 'dev' => true, + ), + 'versions' => array( + '__root__' => array( + 'pretty_version' => 'dev-master', + 'version' => 'dev-master', + 'reference' => 'sourceref-by-default', + 'type' => 'library', + // @phpstan-ignore-next-line + 'install_path' => $dir . '/./', + 'aliases' => array( + '1.10.x-dev', + ), + 'dev_requirement' => false, + ), + 'a/provider' => array( + 'pretty_version' => '1.1', + 'version' => '1.1.0.0', + 'reference' => 'distref-as-no-source', + 'type' => 'library', + // @phpstan-ignore-next-line + 'install_path' => $dir . '/vendor/a/provider', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'a/provider2' => array( + 'pretty_version' => '1.2', + 'version' => '1.2.0.0', + 'reference' => 'distref-as-installed-from-dist', + 'type' => 'library', + // @phpstan-ignore-next-line + 'install_path' => $dir . '/vendor/a/provider2', + 'aliases' => array( + '1.4', + ), + 'dev_requirement' => false, + ), + 'b/replacer' => array( + 'pretty_version' => '2.2', + 'version' => '2.2.0.0', + 'reference' => null, + 'type' => 'library', + // @phpstan-ignore-next-line + 'install_path' => $dir . '/vendor/b/replacer', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'c/c' => array( + 'pretty_version' => '3.0', + 'version' => '3.0.0.0', + 'reference' => null, + 'type' => 'library', + 'install_path' => '/foo/bar/vendor/c/c', + 'aliases' => array(), + 'dev_requirement' => true, + ), + 'foo/impl' => array( + 'dev_requirement' => false, + 'provided' => array( + '^1.1', + '1.2', + '1.4', + '2.0', + ), + ), + 'foo/impl2' => array( + 'dev_requirement' => false, + 'provided' => array( + '2.0', + ), + 'replaced' => array( + '2.2', + ), + ), + 'foo/replaced' => array( + 'dev_requirement' => false, + 'replaced' => array( + '^3.0', + ), + ), + 'meta/package' => array( + 'pretty_version' => '3.0', + 'version' => '3.0.0.0', + 'reference' => null, + 'type' => 'metapackage', + 'install_path' => null, + 'aliases' => array(), + 'dev_requirement' => false, + ) + ), +);