CVE-2024-24821 (published 2024-02-09T00:15Z)

Composer is a dependency Manager for the PHP language. In affected versions several files within the local working directory are included during the invocation of Composer and in the context of the executing user. As such, under certain conditions arbitrary code execution may lead to local privilege escalation, provide lateral user movement or malicious code execution when Composer is invoked within a directory with tampered files. All Composer CLI commands are affected, including composer.phar's self-update. The following scenarios are of high risk: Composer being run with sudo, Pipelines which may execute Composer on untrusted projects, Shared environments with developers who run Composer individually on the same project. This vulnerability has been addressed in versions 2.7.0 and 2.2.23. It is advised that the patched versions are applied at the earliest convenience. Where not possible, the following should be addressed: Remove all sudo composer privileges for all users to mitigate root privilege escalation, and avoid running Composer within an untrusted directory, or if needed, verify that the contents of `vendor/composer/InstalledVersions.php` and `vendor/composer/installed.php` do not include untrusted code.  A reset can also be done on these files by the following:```sh\nrm vendor/composer/installed.php vendor/composer/InstalledVersions.php\ncomposer install --no-scripts --no-plugins\n```

CVSS v3 base score: 7.8, HIGH (CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-829
cwe_name: Inclusion of Functionality from Untrusted Control Sphere
description: The product imports, requires, or includes executable functionality (such as a library) from a source that is outside of the intended control sphere.
extended_description: 
            When including third-party functionality, such as a web widget, library, or other source of functionality, the product must effectively trust that functionality. Without sufficient protection mechanisms, the functionality could be malicious in nature (either by coming from an untrusted source, being spoofed, or being modified in transit from a trusted source). The functionality might also contain its own weaknesses, or grant access to additional functionality and state information that should be kept private to the base system, such as system state information, sensitive application data, or the DOM of a web application.
            This might lead to many different consequences depending on the included functionality, but some examples include injection of malware, information exposure by granting excessive privileges or permissions to the untrusted functionality, DOM-based XSS vulnerabilities, stealing user's cookies, or open redirect to malware (CWE-601).
         
         

Fix information:
repo_url: https://github.com/composer/composer
commit_details: repo_url: https://github.com/composer/composer
author: Jordi Boggiano
author_date: 2024-02-08 14:33:59+01:00
msg: 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
repository: repo_url: https://github.com/composer/composer
repo_name: composer/composer
description: Dependency Manager for PHP
repo_language: PHP
owner: composer
file_changes: filename: Factory.php
change_type: ModificationType.MODIFY
diff: @@ -18,6 +18,7 @@
 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 @@ public function createComposer(IOInterface $io, $localConfig = null, $disablePlu
             $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);
+                }
             }
         }
 

programming_language: PHP
filename: FilesystemRepository.php
change_type: ModificationType.MODIFY
diff: @@ -20,6 +20,7 @@
 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 @@ public function write(bool $devMode, InstallationManager $installationManager)
         }
     }
 
+    /**
+     * 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 @@ private function dumpToPhpCode(array $array = [], int $level = 0): string
 
         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 @@ private function dumpToPhpCode(array $array = [], int $level = 0): string
                 } 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));
             }
         }
 

programming_language: PHP
filename: InstalledVersionsTest.php
change_type: ModificationType.MODIFY
diff: @@ -49,7 +49,7 @@ public function setUp(): void
         $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 @@ public function testGetRootPackage(): void
     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());
     }
 
     /**

programming_language: PHP
filename: FilesystemRepositoryTest.php
change_type: ModificationType.MODIFY
diff: @@ -158,6 +158,7 @@ public function testRepositoryWritesInstalledPhp(): void
         $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 @@ public function testRepositoryWritesInstalledPhp(): void
 
                 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 @@ public function testRepositoryWritesInstalledPhp(): void
             }));
 
         $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);
     }
 
     /**

programming_language: PHP
filename: installed.php
change_type: ModificationType.MODIFY
diff: @@ -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 @@
             '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 @@
             '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 @@
             '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 @@
             '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 @@
             'install_path' => null,
             'aliases' => array(),
             'dev_requirement' => false,
-        )
+        ),
     ),
 );

programming_language: PHP
filename: installed_complex.php
change_type: ModificationType.ADD
diff: @@ -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" . '',
+        ),
+    ),
+);

programming_language: PHP
filename: installed_relative.php
change_type: ModificationType.ADD
diff: @@ -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,
+        )
+    ),
+);

programming_language: PHP