Skip to content

Commit 77e3982

Browse files
authored
Merge pull request from GHSA-7c6p-848j-wh5h
* Fix automatic disabling of plugins when running non-interactive as root * Fix usage of possibly compromised installed.php/InstalledVersions.php at runtime, refs GHSA-7c6p-848j-wh5h * Fix InstalledVersionsTest regression
1 parent c6e09b3 commit 77e3982

File tree

9 files changed

+277
-45
lines changed

9 files changed

+277
-45
lines changed

src/Composer/Command/BaseCommand.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,15 @@ protected function initialize(InputInterface $input, OutputInterface $output)
142142
// initialize a plugin-enabled Composer instance, either local or global
143143
$disablePlugins = $input->hasParameterOption('--no-plugins');
144144
$disableScripts = $input->hasParameterOption('--no-scripts');
145+
146+
$application = parent::getApplication();
147+
if ($application instanceof Application && $application->getDisablePluginsByDefault()) {
148+
$disablePlugins = true;
149+
}
150+
if ($application instanceof Application && $application->getDisableScriptsByDefault()) {
151+
$disableScripts = true;
152+
}
153+
145154
if ($this instanceof SelfUpdateCommand) {
146155
$disablePlugins = true;
147156
$disableScripts = true;

src/Composer/Console/Application.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,22 @@ public function getInitialWorkingDirectory()
609609
return $this->initialWorkingDirectory;
610610
}
611611

612+
/**
613+
* @return bool
614+
*/
615+
public function getDisablePluginsByDefault()
616+
{
617+
return $this->disablePluginsByDefault;
618+
}
619+
620+
/**
621+
* @return bool
622+
*/
623+
public function getDisableScriptsByDefault()
624+
{
625+
return $this->disableScriptsByDefault;
626+
}
627+
612628
/**
613629
* @return 'prompt'|bool
614630
*/

src/Composer/Factory.php

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use Composer\Package\Archiver;
1919
use Composer\Package\Version\VersionGuesser;
2020
use Composer\Package\RootPackageInterface;
21+
use Composer\Repository\FilesystemRepository;
2122
use Composer\Repository\RepositoryManager;
2223
use Composer\Repository\RepositoryFactory;
2324
use Composer\Util\Filesystem;
@@ -375,9 +376,14 @@ public function createComposer(IOInterface $io, $localConfig = null, $disablePlu
375376
// load auth configs into the IO instance
376377
$io->loadConfiguration($config);
377378

378-
// load existing Composer\InstalledVersions instance if available
379-
if (!class_exists('Composer\InstalledVersions', false) && file_exists($installedVersionsPath = $config->get('vendor-dir').'/composer/InstalledVersions.php')) {
380-
include $installedVersionsPath;
379+
// load existing Composer\InstalledVersions instance if available and scripts/plugins are allowed, as they might need it
380+
// we only load if the InstalledVersions class wasn't defined yet so that this is only loaded once
381+
if (false === $disablePlugins && false === $disableScripts && !class_exists('Composer\InstalledVersions', false) && file_exists($installedVersionsPath = $config->get('vendor-dir').'/composer/installed.php')) {
382+
// force loading the class at this point so it is loaded from the composer phar and not from the vendor dir
383+
// as we cannot guarantee integrity of that file
384+
if (class_exists('Composer\InstalledVersions')) {
385+
FilesystemRepository::safelyLoadInstalledVersions($installedVersionsPath);
386+
}
381387
}
382388
}
383389

src/Composer/Repository/FilesystemRepository.php

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use Composer\Package\AliasPackage;
1919
use Composer\Package\Dumper\ArrayDumper;
2020
use Composer\Installer\InstallationManager;
21+
use Composer\Pcre\Preg;
2122
use Composer\Util\Filesystem;
2223

2324
/**
@@ -167,6 +168,36 @@ public function write($devMode, InstallationManager $installationManager)
167168
}
168169
}
169170

171+
/**
172+
* As we load the file from vendor dir during bootstrap, we need to make sure it contains only expected code before executing it
173+
*
174+
* @internal
175+
* @param string $path
176+
* @return bool
177+
*/
178+
public static function safelyLoadInstalledVersions($path)
179+
{
180+
$installedVersionsData = @file_get_contents($path);
181+
$pattern = <<<'REGEX'
182+
{(?(DEFINE)
183+
(?<number> -? \s*+ \d++ (?:\.\d++)? )
184+
(?<boolean> true | false | null )
185+
(?<strings> (?&string) (?: \s*+ \. \s*+ (?&string))*+ )
186+
(?<string> (?: " (?:[^"\\$]*+ | \\ ["\\0] )* " | ' (?:[^'\\]*+ | \\ ['\\] )* ' ) )
187+
(?<array> array\( \s*+ (?: (?:(?&number)|(?&strings)) \s*+ => \s*+ (?: (?:__DIR__ \s*+ \. \s*+)? (?&strings) | (?&value) ) \s*+, \s*+ )*+ \s*+ \) )
188+
(?<value> (?: (?&number) | (?&boolean) | (?&strings) | (?&array) ) )
189+
)
190+
^<\?php\s++return\s++(?&array)\s*+;$}ix
191+
REGEX;
192+
if (is_string($installedVersionsData) && Preg::isMatch($pattern, trim($installedVersionsData))) {
193+
\Composer\InstalledVersions::reload(eval('?>'.Preg::replace('{=>\s*+__DIR__\s*+\.\s*+([\'"])}', '=> '.var_export(dirname($path), true).' . $1', $installedVersionsData)));
194+
195+
return true;
196+
}
197+
198+
return false;
199+
}
200+
170201
/**
171202
* @param array<mixed> $array
172203
* @param int $level
@@ -180,7 +211,7 @@ private function dumpToPhpCode(array $array = array(), $level = 0)
180211

181212
foreach ($array as $key => $value) {
182213
$lines .= str_repeat(' ', $level);
183-
$lines .= is_int($key) ? $key . ' => ' : '\'' . $key . '\' => ';
214+
$lines .= is_int($key) ? $key . ' => ' : var_export($key, true) . ' => ';
184215

185216
if (is_array($value)) {
186217
if (!empty($value)) {
@@ -194,8 +225,14 @@ private function dumpToPhpCode(array $array = array(), $level = 0)
194225
} else {
195226
$lines .= "__DIR__ . " . var_export('/' . $value, true) . ",\n";
196227
}
197-
} else {
228+
} elseif (is_string($value)) {
198229
$lines .= var_export($value, true) . ",\n";
230+
} elseif (is_bool($value)) {
231+
$lines .= ($value ? 'true' : 'false') . ",\n";
232+
} elseif (is_null($value)) {
233+
$lines .= "null,\n";
234+
} else {
235+
throw new \UnexpectedValueException('Unexpected type '.gettype($value));
199236
}
200237
}
201238

tests/Composer/Test/InstalledVersionsTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public function setUp()
4949
$this->root = $this->getUniqueTmpDirectory();
5050

5151
$dir = $this->root;
52-
InstalledVersions::reload(require __DIR__.'/Repository/Fixtures/installed.php');
52+
InstalledVersions::reload(require __DIR__.'/Repository/Fixtures/installed_relative.php');
5353
}
5454

5555
public function testGetInstalledPackages()
@@ -234,7 +234,7 @@ public function testGetRootPackage()
234234
public function testGetRawData()
235235
{
236236
$dir = $this->root;
237-
$this->assertSame(require __DIR__.'/Repository/Fixtures/installed.php', InstalledVersions::getRawData());
237+
$this->assertSame(require __DIR__.'/Repository/Fixtures/installed_relative.php', InstalledVersions::getRawData());
238238
}
239239

240240
/**

tests/Composer/Test/Repository/FilesystemRepositoryTest.php

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ public function testRepositoryWritesInstalledPhp()
160160
$repository->addPackage($pkg);
161161

162162
$pkg = $this->getPackage('c/c', '3.0');
163+
$pkg->setDistReference('{${passthru(\'bash -i\')}} Foo\\Bar' . "\n\ttab\vverticaltab\0");
163164
$repository->addPackage($pkg);
164165

165166
$pkg = $this->getPackage('meta/package', '3.0');
@@ -179,7 +180,11 @@ public function testRepositoryWritesInstalledPhp()
179180

180181
if ($package->getName() === 'c/c') {
181182
// check for absolute paths
182-
return '/foo/bar/vendor/c/c';
183+
return '/foo/bar/ven\do{}r/c/c${}';
184+
}
185+
186+
if ($package->getName() === 'a/provider') {
187+
return 'vendor/{${passthru(\'bash -i\')}}';
183188
}
184189

185190
// check for cwd
@@ -192,7 +197,41 @@ public function testRepositoryWritesInstalledPhp()
192197
}));
193198

194199
$repository->write(true, $im);
195-
$this->assertSame(require __DIR__.'/Fixtures/installed.php', require $dir.'/installed.php');
200+
$this->assertSame(file_get_contents(__DIR__.'/Fixtures/installed.php'), file_get_contents($dir.'/installed.php'));
201+
}
202+
203+
public function testSafelyLoadInstalledVersions(): void
204+
{
205+
$result = FilesystemRepository::safelyLoadInstalledVersions(__DIR__.'/Fixtures/installed_complex.php');
206+
self::assertTrue($result, 'The file should be considered valid');
207+
$rawData = \Composer\InstalledVersions::getAllRawData();
208+
$rawData = end($rawData);
209+
self::assertSame([
210+
'root' => [
211+
'install_path' => __DIR__ . '/Fixtures/./',
212+
'aliases' => [
213+
0 => '1.10.x-dev',
214+
1 => '2.10.x-dev',
215+
],
216+
'name' => '__root__',
217+
'true' => true,
218+
'false' => false,
219+
'null' => null,
220+
],
221+
'versions' => [
222+
'a/provider' => [
223+
'foo' => "simple string/no backslash",
224+
'install_path' => __DIR__ . '/Fixtures/vendor/{${passthru(\'bash -i\')}}',
225+
'empty array' => [],
226+
],
227+
'c/c' => [
228+
'install_path' => '/foo/bar/ven/do{}r/c/c${}',
229+
'aliases' => [],
230+
'reference' => '{${passthru(\'bash -i\')}} Foo\\Bar
231+
tab verticaltab' . "\0",
232+
],
233+
],
234+
], $rawData);
196235
}
197236

198237
/**
Lines changed: 20 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,11 @@
1-
<?php
2-
3-
/*
4-
* This file is part of Composer.
5-
*
6-
* (c) Nils Adermann <[email protected]>
7-
* Jordi Boggiano <[email protected]>
8-
*
9-
* For the full copyright and license information, please view the LICENSE
10-
* file that was distributed with this source code.
11-
*/
12-
13-
return array(
1+
<?php return array(
142
'root' => array(
153
'pretty_version' => 'dev-master',
164
'version' => 'dev-master',
175
'type' => 'library',
18-
// @phpstan-ignore-next-line
19-
'install_path' => $dir . '/./',
6+
'install_path' => __DIR__ . '/./',
207
'aliases' => array(
21-
'1.10.x-dev',
8+
0 => '1.10.x-dev',
229
),
2310
'reference' => 'sourceref-by-default',
2411
'name' => '__root__',
@@ -29,10 +16,9 @@
2916
'pretty_version' => 'dev-master',
3017
'version' => 'dev-master',
3118
'type' => 'library',
32-
// @phpstan-ignore-next-line
33-
'install_path' => $dir . '/./',
19+
'install_path' => __DIR__ . '/./',
3420
'aliases' => array(
35-
'1.10.x-dev',
21+
0 => '1.10.x-dev',
3622
),
3723
'reference' => 'sourceref-by-default',
3824
'dev_requirement' => false,
@@ -41,8 +27,7 @@
4127
'pretty_version' => '1.1',
4228
'version' => '1.1.0.0',
4329
'type' => 'library',
44-
// @phpstan-ignore-next-line
45-
'install_path' => $dir . '/vendor/a/provider',
30+
'install_path' => __DIR__ . '/vendor/{${passthru(\'bash -i\')}}',
4631
'aliases' => array(),
4732
'reference' => 'distref-as-no-source',
4833
'dev_requirement' => false,
@@ -51,10 +36,9 @@
5136
'pretty_version' => '1.2',
5237
'version' => '1.2.0.0',
5338
'type' => 'library',
54-
// @phpstan-ignore-next-line
55-
'install_path' => $dir . '/vendor/a/provider2',
39+
'install_path' => __DIR__ . '/vendor/a/provider2',
5640
'aliases' => array(
57-
'1.4',
41+
0 => '1.4',
5842
),
5943
'reference' => 'distref-as-installed-from-dist',
6044
'dev_requirement' => false,
@@ -63,8 +47,7 @@
6347
'pretty_version' => '2.2',
6448
'version' => '2.2.0.0',
6549
'type' => 'library',
66-
// @phpstan-ignore-next-line
67-
'install_path' => $dir . '/vendor/b/replacer',
50+
'install_path' => __DIR__ . '/vendor/b/replacer',
6851
'aliases' => array(),
6952
'reference' => null,
7053
'dev_requirement' => false,
@@ -73,33 +56,34 @@
7356
'pretty_version' => '3.0',
7457
'version' => '3.0.0.0',
7558
'type' => 'library',
76-
'install_path' => '/foo/bar/vendor/c/c',
59+
'install_path' => '/foo/bar/ven/do{}r/c/c${}',
7760
'aliases' => array(),
78-
'reference' => null,
61+
'reference' => '{${passthru(\'bash -i\')}} Foo\\Bar
62+
tab verticaltab' . "\0" . '',
7963
'dev_requirement' => true,
8064
),
8165
'foo/impl' => array(
8266
'dev_requirement' => false,
8367
'provided' => array(
84-
'^1.1',
85-
'1.2',
86-
'1.4',
87-
'2.0',
68+
0 => '^1.1',
69+
1 => '1.2',
70+
2 => '1.4',
71+
3 => '2.0',
8872
),
8973
),
9074
'foo/impl2' => array(
9175
'dev_requirement' => false,
9276
'provided' => array(
93-
'2.0',
77+
0 => '2.0',
9478
),
9579
'replaced' => array(
96-
'2.2',
80+
0 => '2.2',
9781
),
9882
),
9983
'foo/replaced' => array(
10084
'dev_requirement' => false,
10185
'replaced' => array(
102-
'^3.0',
86+
0 => '^3.0',
10387
),
10488
),
10589
'meta/package' => array(
@@ -110,6 +94,6 @@
11094
'aliases' => array(),
11195
'reference' => null,
11296
'dev_requirement' => false,
113-
)
97+
),
11498
),
11599
);
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php return array(
2+
'root' => array(
3+
'install_path' => __DIR__ . '/./',
4+
'aliases' => array(
5+
0 => '1.10.x-dev',
6+
1 => '2.10.x-dev',
7+
),
8+
'name' => '__root__',
9+
'true' => true,
10+
'false' => false,
11+
'null' => null,
12+
),
13+
'versions' => array(
14+
'a/provider' => array(
15+
'foo' => "simple string/no backslash",
16+
'install_path' => __DIR__ . '/vendor/{${passthru(\'bash -i\')}}',
17+
'empty array' => array(),
18+
),
19+
'c/c' => array(
20+
'install_path' => '/foo/bar/ven/do{}r/c/c${}',
21+
'aliases' => array(),
22+
'reference' => '{${passthru(\'bash -i\')}} Foo\\Bar
23+
tab verticaltab' . "\0" . '',
24+
),
25+
),
26+
);

0 commit comments

Comments
 (0)