From 4bdb77926affdcad0c3c1bac59e99aadfaf91f82 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 14 Apr 2026 13:31:52 +0200 Subject: [PATCH 1/5] Reverting release version changes --- src/Composer/Composer.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Composer/Composer.php b/src/Composer/Composer.php index d96581f525cf..6c654cc212c3 100644 --- a/src/Composer/Composer.php +++ b/src/Composer/Composer.php @@ -51,10 +51,10 @@ class Composer extends PartialComposer * * @see getVersion() */ - public const VERSION = '2.9.7'; - public const BRANCH_ALIAS_VERSION = ''; - public const RELEASE_DATE = '2026-04-14 13:31:52'; - public const SOURCE_VERSION = ''; + public const VERSION = '@package_version@'; + public const BRANCH_ALIAS_VERSION = '@package_branch_alias_version@'; + public const RELEASE_DATE = '@release_date@'; + public const SOURCE_VERSION = '2.9.999-dev+source'; /** * Version number of the internal composer-runtime-api package From 3f5e7f9fbfa541137d6d1d5643ec3b718e9d5039 Mon Sep 17 00:00:00 2001 From: Philipp Scheit Date: Wed, 13 May 2026 09:00:52 +0200 Subject: [PATCH 2/5] Fix regexp to support new GitHub installation tokens format (#12853) --- src/Composer/IO/BaseIO.php | 5 +- src/Composer/Util/GitHub.php | 2 +- tests/Composer/Test/IO/BaseIOTest.php | 99 +++++++++++++++++++++++++++ 3 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 tests/Composer/Test/IO/BaseIOTest.php diff --git a/src/Composer/IO/BaseIO.php b/src/Composer/IO/BaseIO.php index fc3155c3aa33..9f2a64654d82 100644 --- a/src/Composer/IO/BaseIO.php +++ b/src/Composer/IO/BaseIO.php @@ -139,9 +139,10 @@ public function loadConfiguration(Config $config) // allowed chars for GH tokens are from https://github.blog/changelog/2021-03-04-authentication-token-format-updates/ // plus dots which were at some point used for GH app integration tokens - if (!Preg::isMatch('{^[.A-Za-z0-9_]+$}', $token)) { - throw new \UnexpectedValueException('Your github oauth token for '.$domain.' contains invalid characters: "'.$token.'"'); + if (!Preg::isMatch('{^[.A-Za-z0-9_-]+$}', $token)) { + throw new \UnexpectedValueException('Your github oauth token for '.$domain.' contains invalid characters.'); } + $this->checkAndSetAuthentication($domain, $token, 'x-oauth-basic'); } diff --git a/src/Composer/Util/GitHub.php b/src/Composer/Util/GitHub.php index eec5939beab0..b97085baaafb 100644 --- a/src/Composer/Util/GitHub.php +++ b/src/Composer/Util/GitHub.php @@ -23,7 +23,7 @@ */ class GitHub { - public const GITHUB_TOKEN_REGEX = '{^([a-f0-9]{12,}|gh[a-z]_[a-zA-Z0-9_]+|github_pat_[a-zA-Z0-9_]+)$}'; + public const GITHUB_TOKEN_REGEX = '{^([a-f0-9]{12,}|gh[a-z]_[a-zA-Z0-9_.-]+|github_pat_[a-zA-Z0-9_]+)$}'; /** @var IOInterface */ protected $io; diff --git a/tests/Composer/Test/IO/BaseIOTest.php b/tests/Composer/Test/IO/BaseIOTest.php new file mode 100644 index 000000000000..b899d86476ee --- /dev/null +++ b/tests/Composer/Test/IO/BaseIOTest.php @@ -0,0 +1,99 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test\IO; + +use Composer\Config; +use Composer\IO\BufferIO; +use Composer\Test\TestCase; + +class BaseIOTest extends TestCase +{ + /** + * @dataProvider provideValidGithubTokens + */ + public function testLoadConfigurationAcceptsValidGithubToken(string $token): void + { + $io = new BufferIO(); + $config = new Config(false); + $config->merge(['config' => ['github-oauth' => ['github.com' => $token]]]); + + $io->loadConfiguration($config); + + $auth = $io->getAuthentication('github.com'); + self::assertSame($token, $auth['username']); + self::assertSame('x-oauth-basic', $auth['password']); + } + + /** @return array */ + public static function provideValidGithubTokens(): array + { + return [ + 'legacy 40-hex PAT' => ['8a7f2c1bdc4e9f06a3b7c2e9d4f1a8b6c5d7e0f2'], + 'ghp_ flat token' => ['ghp_n3K9wQ2eL5bV8mY1pX4cZ7aR0fT6sH3uJ8oI'], + 'gho_ flat token' => ['gho_M2pQ7vR4xL9eK6bN1cT8aZ0sJ3wY5fH7uG2d'], + 'ghu_ flat token' => ['ghu_R5tY8wA1xC4eK7bN0pV3mL6sH9uJ2gD5fQ8z'], + 'ghs_ flat token' => ['ghs_K7bN3pV5mL8eR2tY9wA1xC4sH6uJ0gD3fQ8z'], + 'ghr_ flat token' => ['ghr_X9aZ2sJ5wY8fH1uG4dR7bN0pV3mL6eK2tQ5c'], + // shivammathur/setup-php style: ghs__.. + // base64url alphabet includes '-' which the old regex rejected + 'ghs_ structured installation token (jwt body)' => [ + 'ghs_1234567890_eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJjb21wb3NlciJ9.aB-cDef_GHIjkl-mnoPQR0123456', + ], + 'github_pat_ fine-grained PAT' => [ + 'github_pat_11ABCDEFG0aB1cD2eF3gH4_n3K9wQ2eL5bV8mY1pX4cZ7aR0fT6sH3uJ8oI2pQ7vR4xL9eK6bN', + ], + ]; + } + + /** + * @dataProvider provideUrlBreakingGithubTokens + */ + public function testLoadConfigurationRejectsTokenWithUrlBreakingCharacters(string $token, string $offending): void + { + $io = new BufferIO(); + $config = new Config(false); + $config->merge(['config' => ['github-oauth' => ['github.com' => $token]]]); + + try { + $io->loadConfiguration($config); + self::fail('Expected loadConfiguration to reject token containing '.$offending); + } catch (\UnexpectedValueException $e) { + // Defect #1: the rejected token must not be echoed back into the + // exception message — Symfony Console renders it to stderr and CI + // log shippers / GitHub Actions secret masking do not reliably + // strip it from the framed error block. + self::assertStringNotContainsString( + $token, + $e->getMessage(), + 'Exception message must not leak the rejected token value.' + ); + } + } + + /** @return array */ + public static function provideUrlBreakingGithubTokens(): array + { + return [ + 'contains @ (userinfo separator)' => ['ghp_AAAA@evil.example.com', '@'], + 'contains : (basic-auth user:pass split)' => ['ghp_AAAA:extra', ':'], + 'contains / (path separator)' => ['ghp_AAA/BBB', '/'], + 'contains backslash' => ['ghp_AAA\\BBB', '\\'], + 'contains ? (query separator)' => ['ghp_AAA?x=1', '?'], + 'contains # (fragment)' => ['ghp_AAA#frag', '#'], + 'contains space' => ['ghp_AAA BBB', 'space'], + 'contains tab' => ["ghp_AAA\tBBB", 'tab'], + 'contains CR' => ["ghp_AAA\rBBB", 'CR'], + 'contains LF (header injection)' => ["ghp_AAA\nX-Evil: 1", 'LF'], + ]; + } +} From bd6cda27aa11f565aa2d0fa2ade191ed74e3d158 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 13 May 2026 09:10:28 +0200 Subject: [PATCH 3/5] Update changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9d4d1e554ba..5b75966b31c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +### [2.9.8] 2026-05-13 + + * Security: Fixed GitHub token validation and disclosure (GHSA-f9f8-rm49-7jv2) + ### [2.9.7] 2026-04-14 * Fixes regression calling custom script command aliases that are called a substring of a composer command (#12802) @@ -2141,6 +2145,7 @@ * Initial release +[2.9.8]: https://github.com/composer/composer/compare/2.9.7...2.9.8 [2.9.7]: https://github.com/composer/composer/compare/2.9.6...2.9.7 [2.9.6]: https://github.com/composer/composer/compare/2.9.5...2.9.6 [2.9.5]: https://github.com/composer/composer/compare/2.9.4...2.9.5 From fa0f839011f5fdf20af5fa2c0fd485ad0ebb6632 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 13 May 2026 09:22:56 +0200 Subject: [PATCH 4/5] Fix ci --- .github/workflows/autoloader.yml | 1 + .github/workflows/conductor.yaml | 1 + .github/workflows/continuous-integration.yml | 4 ++-- .github/workflows/lint.yml | 1 + .github/workflows/php32bit.yml | 2 +- .github/workflows/phpstan.yml | 1 + .github/workflows/release.yml | 1 + 7 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/autoloader.yml b/.github/workflows/autoloader.yml index 23a681fd9b6b..1f77b706e094 100644 --- a/.github/workflows/autoloader.yml +++ b/.github/workflows/autoloader.yml @@ -33,6 +33,7 @@ jobs: extensions: "intl, zip" ini-values: "memory_limit=-1" php-version: "5.6" + tools: "composer:snapshot" - name: "Check the autoloader can be executed" run: "php main.php" diff --git a/.github/workflows/conductor.yaml b/.github/workflows/conductor.yaml index a5db05e6f05e..6b9a3b70cfd8 100644 --- a/.github/workflows/conductor.yaml +++ b/.github/workflows/conductor.yaml @@ -28,6 +28,7 @@ jobs: with: php-version: "latest" coverage: "none" + tools: "composer:snapshot" # See the Conductor GitHub Action at https://github.com/packagist/conductor-github-action - name: "Running Conductor" diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 821e57bc7af3..b86855a3c8c1 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -75,7 +75,7 @@ jobs: extensions: "intl, zip" ini-values: "memory_limit=-1, phar.readonly=0, error_reporting=E_ALL, display_errors=On, display_startup_errors=On" php-version: "${{ matrix.php-version }}" - tools: composer + tools: "composer:snapshot" - name: "Handle lowest dependencies update" if: "contains(matrix.dependencies, 'lowest')" @@ -146,7 +146,7 @@ jobs: extensions: "intl, zip" ini-values: "memory_limit=-1, phar.readonly=0, error_reporting=E_ALL, display_errors=On, display_startup_errors=On" php-version: "7.4" - tools: composer + tools: "composer:snapshot" - name: "Install dependencies" run: "composer install ${{ env.COMPOSER_FLAGS }}" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index ea8656b6fe62..377585561417 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -30,6 +30,7 @@ jobs: with: php-version: "${{ matrix.php-version }}" coverage: none + tools: "composer:snapshot" - uses: ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520 # 3.1.1 with: diff --git a/.github/workflows/php32bit.yml b/.github/workflows/php32bit.yml index e09b818a914c..fb99c643a73b 100644 --- a/.github/workflows/php32bit.yml +++ b/.github/workflows/php32bit.yml @@ -30,7 +30,7 @@ jobs: extensions: "intl, zip" ini-values: "memory_limit=-1, phar.readonly=0, error_reporting=E_ALL, display_errors=On, display_startup_errors=On" php-version: "8.4" - tools: composer + tools: "composer:snapshot" - name: "Install dependencies from composer.lock using composer binary provided by system" run: "composer install ${{ env.COMPOSER_FLAGS }}" diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml index 4c38f99f150d..809460d841ca 100644 --- a/.github/workflows/phpstan.yml +++ b/.github/workflows/phpstan.yml @@ -40,6 +40,7 @@ jobs: extensions: "intl, zip" ini-values: "memory_limit=-1" php-version: "${{ matrix.php-version }}" + tools: "composer:snapshot" - name: "Determine composer cache directory" id: "determine-composer-cache-directory" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bf170a7e4d64..0485d9c7cc7e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,6 +28,7 @@ jobs: extensions: "intl" ini-values: "memory_limit=-1" php-version: "8.4" + tools: "composer:snapshot" - name: "Install dependencies from composer.lock using composer binary provided by system" run: "composer install ${{ env.COMPOSER_FLAGS }}" From 39ee8baff8e97a1b657bbfcd6a236ff93a5efbb2 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 13 May 2026 09:28:38 +0200 Subject: [PATCH 5/5] Release 2.9.8 --- src/Composer/Composer.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Composer/Composer.php b/src/Composer/Composer.php index 6c654cc212c3..a07c52b92a9b 100644 --- a/src/Composer/Composer.php +++ b/src/Composer/Composer.php @@ -51,10 +51,10 @@ class Composer extends PartialComposer * * @see getVersion() */ - public const VERSION = '@package_version@'; - public const BRANCH_ALIAS_VERSION = '@package_branch_alias_version@'; - public const RELEASE_DATE = '@release_date@'; - public const SOURCE_VERSION = '2.9.999-dev+source'; + public const VERSION = '2.9.8'; + public const BRANCH_ALIAS_VERSION = ''; + public const RELEASE_DATE = '2026-05-13 09:28:38'; + public const SOURCE_VERSION = ''; /** * Version number of the internal composer-runtime-api package