CVE-2024-24821 (PHP, published 2024-02-09T00:15Z)
Description: value: 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
rm vendor/composer/installed.php vendor/composer/InstalledVersions.php
composer install --no-scripts --no-plugins
```
Severity: nan (CVSS v3 base score: 7.8)
Weakness: CWE-829 \u2014 Inclusion of Functionality from Untrusted Control Sphere
The product imports, requires, or includes executable functionality (such as a library) from a source that is outside of the intended control sphere.
Repository: https://github.com/composer/composer
Files changed:
src/Composer/Factory.php
src/Composer/Repository/FilesystemRepository.php
tests/Composer/Test/InstalledVersionsTest.php
tests/Composer/Test/Repository/FilesystemRepositoryTest.php
tests/Composer/Test/Repository/Fixtures/installed.php
tests/Composer/Test/Repository/Fixtures/installed_complex.php
tests/Composer/Test/Repository/Fixtures/installed_relative.php
Commit message: 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

Vulnerable code:
            if (false === $disablePlugins && false === $disableScripts && !class_exists('Composer\InstalledVersions', false) && file_exists($installedVersionsPath = $config->get('vendor-dir').'/composer/InstalledVersions.php')) {
                include $installedVersionsPath;
            $lines .= is_int($key) ? $key . ' => ' : '\'' . $key . '\' => ';
            } else {
        InstalledVersions::reload(require __DIR__.'/Repository/Fixtures/installed.php');
        $this->assertSame(require __DIR__.'/Repository/Fixtures/installed.php', InstalledVersions::getRawData());
                    return '/foo/bar/vendor/c/c';
        $this->assertSame(require __DIR__.'/Fixtures/installed.php', require $dir.'/installed.php');
<?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(
        // @phpstan-ignore-next-line
        'install_path' => $dir . '/./',
            '1.10.x-dev',
            // @phpstan-ignore-next-line
            'install_path' => $dir . '/./',
                '1.10.x-dev',
            // @phpstan-ignore-next-line
            'install_path' => $dir . '/vendor/a/provider',
            // @phpstan-ignore-next-line
            'install_path' => $dir . '/vendor/a/provider2',
              '1.4',
            // @phpstan-ignore-next-line
            'install_path' => $dir . '/vendor/b/replacer',
            'reference' => null,
            'install_path' => '/foo/bar/vendor/c/c',
                '^1.1',
                '1.2',
                '1.4',
                '2.0',
                '2.0',
                '2.2',
                '^3.0',
        )

Fixed code:
use Composer\Repository\FilesystemRepository;
            // 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);
                }
use Composer\Pcre\Preg;
    /**
     * 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;
    }

            $lines .= is_int($key) ? $key . ' => ' : var_export($key, true) . ' => ';
            } elseif (is_string($value)) {
            } elseif (is_bool($value)) {
                $lines .= ($value ? 'true' : 'false') . ",\n";
            } elseif (is_null($value)) {
                $lines .= "null,\n";
            } else {
                throw new \UnexpectedValueException('Unexpected type '.gettype($value));
        InstalledVersions::reload(require __DIR__.'/Repository/Fixtures/installed_relative.php');
        $this->assertSame(require __DIR__.'/Repository/Fixtures/installed_relative.php', InstalledVersions::getRawData());
        $pkg->setDistReference('{${passthru(\'bash -i\')}} Foo\\Bar' . "\n\ttab\vverticaltab\0");
                    return '/foo/bar/ven\do{}r/c/c${}';
                }

                if ($package->getName() === 'a/provider') {
                    return 'vendor/{${passthru(\'bash -i\')}}';
        $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);
<?php return array(
        'install_path' => __DIR__ . '/./',
            0 => '1.10.x-dev',
            'install_path' => __DIR__ . '/./',
                0 => '1.10.x-dev',
            'install_path' => __DIR__ . '/vendor/{${passthru(\'bash -i\')}}',
            'install_path' => __DIR__ . '/vendor/a/provider2',
                0 => '1.4',
            'install_path' => __DIR__ . '/vendor/b/replacer',
            'reference' => '{${passthru(\'bash -i\')}} Foo\\Bar
	tabverticaltab' . "\0" . '',
            'install_path' => '/foo/bar/ven/do{}r/c/c${}',
                0 => '^1.1',
                1 => '1.2',
                2 => '1.4',
                3 => '2.0',
                0 => '2.0',
                0 => '2.2',
                0 => '^3.0',
        ),
<?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" . '',
        ),
    ),
);
<?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,
        )
    ),
);