1
0
Fork 0

Merge pull request from GHSA-7c6p-848j-wh5h

* Fix usage of possibly compromised installed.php/InstalledVersions.php at runtime, refs GHSA-7c6p-848j-wh5h

* Fix InstalledVersionsTest regression
pull/11842/head
Jordi Boggiano 2024-02-08 14:33:59 +01:00 committed by GitHub
parent 7442981364
commit 64e4eb356b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 237 additions and 44 deletions

View File

@ -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);
}
}
}

View File

@ -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)
(?<number> -? \s*+ \d++ (?:\.\d++)? )
(?<boolean> true | false | null )
(?<strings> (?&string) (?: \s*+ \. \s*+ (?&string))*+ )
(?<string> (?: " (?:[^"\\$]*+ | \\ ["\\0] )* " | ' (?:[^'\\]*+ | \\ ['\\] )* ' ) )
(?<array> array\( \s*+ (?: (?:(?&number)|(?&strings)) \s*+ => \s*+ (?: (?:__DIR__ \s*+ \. \s*+)? (?&strings) | (?&value) ) \s*+, \s*+ )*+ \s*+ \) )
(?<value> (?: (?&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<mixed> $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));
}
}

View File

@ -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());
}
/**

View File

@ -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);
}
/**

View File

@ -1,26 +1,13 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
return array(
<?php return array(
'root' => 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,
)
),
),
);

View File

@ -0,0 +1,26 @@
<?php return array(
'root' => 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" . '',
),
),
);

View File

@ -0,0 +1,103 @@
<?php return array(
'root' => 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,
)
),
);