--- Begin Message ---
Package: release.debian.org
Severity: normal
Tags: trixie
X-Debbugs-Cc: [email protected]
Control: affects -1 + src:composer
User: [email protected]
Usertags: pu
Hi,
As agreed with the security team, I’d like to address a GitHub token
leak [CVE-2026-45793] via p-u. The change is just a regex match on code
that may not be used outside of GitHub infrastructure, and the testsuite
is updated to check for it.
[ Checklist ]
[x] *all* changes are documented in the d/changelog
[x] I reviewed all changes and I approve them
[x] attach debdiff against the package in (old)stable
[x] the issue is verified as fixed in unstable
Cheers,
taffit
diff -Nru composer-2.8.8/debian/changelog composer-2.8.8/debian/changelog
--- composer-2.8.8/debian/changelog 2026-04-15 10:50:08.000000000 +0200
+++ composer-2.8.8/debian/changelog 2026-05-14 10:37:40.000000000 +0200
@@ -1,3 +1,10 @@
+composer (2.8.8-1+deb13u3) trixie; urgency=medium
+
+ * Fix regexp to support new GitHub installation tokens format
+ (GHSA-f9f8-rm49-7jv2) [CVE-2026-45793]
+
+ -- David Prévot <[email protected]> Thu, 14 May 2026 10:37:40 +0200
+
composer (2.8.8-1+deb13u2) trixie; urgency=medium
* Fix command injection via malicious Perforce repository definition
diff -Nru composer-2.8.8/debian/patches/0020-ProcessExecutor-mask-GitHub-fine-grained-access-toke.patch composer-2.8.8/debian/patches/0020-ProcessExecutor-mask-GitHub-fine-grained-access-toke.patch
--- composer-2.8.8/debian/patches/0020-ProcessExecutor-mask-GitHub-fine-grained-access-toke.patch 1970-01-01 01:00:00.000000000 +0100
+++ composer-2.8.8/debian/patches/0020-ProcessExecutor-mask-GitHub-fine-grained-access-toke.patch 2026-05-14 10:37:03.000000000 +0200
@@ -0,0 +1,68 @@
+From: Stephan <[email protected]>
+Date: Thu, 21 Aug 2025 10:07:24 +0100
+Subject: ProcessExecutor: mask GitHub fine grained access tokens similar to
+ URL (#12487)
+
+Origin: upstream, https://github.com/composer/composer/commit/af81eac1816effad4fa959eb79b73ba214254540
+---
+ src/Composer/Util/GitHub.php | 2 ++
+ src/Composer/Util/ProcessExecutor.php | 4 ++--
+ src/Composer/Util/Url.php | 4 ++--
+ tests/Composer/Test/Util/ProcessExecutorTest.php | 1 +
+ 4 files changed, 7 insertions(+), 4 deletions(-)
+
+diff --git a/src/Composer/Util/GitHub.php b/src/Composer/Util/GitHub.php
+index 64ee4f5..c6ad90f 100644
+--- a/src/Composer/Util/GitHub.php
++++ b/src/Composer/Util/GitHub.php
+@@ -23,6 +23,8 @@ use Composer\Pcre\Preg;
+ */
+ class GitHub
+ {
++ 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;
+ /** @var Config */
+diff --git a/src/Composer/Util/ProcessExecutor.php b/src/Composer/Util/ProcessExecutor.php
+index 11db709..91a5c40 100644
+--- a/src/Composer/Util/ProcessExecutor.php
++++ b/src/Composer/Util/ProcessExecutor.php
+@@ -483,8 +483,8 @@ class ProcessExecutor
+
+ $commandString = is_string($command) ? $command : implode(' ', array_map(self::class.'::escape', $command));
+ $safeCommand = Preg::replaceCallback('{://(?P<user>[^:/\s]+):(?P<password>[^@\s/]+)@}i', static function ($m): string {
+- // if the username looks like a long (12char+) hex string, or a modern github token (e.g. ghp_xxx) we obfuscate that
+- if (Preg::isMatch('{^([a-f0-9]{12,}|gh[a-z]_[a-zA-Z0-9_]+)$}', $m['user'])) {
++ // if the username looks like a long (12char+) hex string, or a modern github token (e.g. ghp_xxx, github_pat_xxx) we obfuscate that
++ if (Preg::isMatch(GitHub::GITHUB_TOKEN_REGEX, $m['user'])) {
+ return '://***:***@';
+ }
+ if (Preg::isMatch('{^[a-f0-9]{12,}$}', $m['user'])) {
+diff --git a/src/Composer/Util/Url.php b/src/Composer/Util/Url.php
+index 32f6134..7e615fe 100644
+--- a/src/Composer/Util/Url.php
++++ b/src/Composer/Util/Url.php
+@@ -110,8 +110,8 @@ class Url
+ $url = Preg::replace('{([&?]access_token=)[^&]+}', '$1***', $url);
+
+ $url = Preg::replaceCallback('{^(?P<prefix>[a-z0-9]+://)?(?P<user>[^:/\s@]+):(?P<password>[^@\s/]+)@}i', static function ($m): string {
+- // if the username looks like a long (12char+) hex string, or a modern github token (e.g. ghp_xxx) we obfuscate that
+- if (Preg::isMatch('{^([a-f0-9]{12,}|gh[a-z]_[a-zA-Z0-9_]+|github_pat_[a-zA-Z0-9_]+)$}', $m['user'])) {
++ // if the username looks like a long (12char+) hex string, or a modern github token (e.g. ghp_xxx, github_pat_xxx) we obfuscate that
++ if (Preg::isMatch(GitHub::GITHUB_TOKEN_REGEX, $m['user'])) {
+ return $m['prefix'].'***:***@';
+ }
+
+diff --git a/tests/Composer/Test/Util/ProcessExecutorTest.php b/tests/Composer/Test/Util/ProcessExecutorTest.php
+index 28aca39..da27847 100644
+--- a/tests/Composer/Test/Util/ProcessExecutorTest.php
++++ b/tests/Composer/Test/Util/ProcessExecutorTest.php
+@@ -82,6 +82,7 @@ class ProcessExecutorTest extends TestCase
+ ['echo https://foo:[email protected]/', 'echo https://foo:***@example.org/'],
+ ['echo http://[email protected]', 'echo http://[email protected]'],
+ ['echo http://abcdef1234567890234578:[email protected]/', 'echo http://***:***@github.com/'],
++ ['echo http://github_pat_1234567890abcdefghijkl_1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVW:[email protected]/', 'echo http://***:***@github.com/'],
+ ["svn ls --verbose --non-interactive --username 'foo' --password 'bar' 'https://foo.example.org/svn/'", "svn ls --verbose --non-interactive --username 'foo' --password '***' 'https://foo.example.org/svn/'"],
+ ["svn ls --verbose --non-interactive --username 'foo' --password 'bar \'bar' 'https://foo.example.org/svn/'", "svn ls --verbose --non-interactive --username 'foo' --password '***' 'https://foo.example.org/svn/'"],
+ ];
diff -Nru composer-2.8.8/debian/patches/0021-Fix-regexp-to-support-new-GitHub-installation-tokens.patch composer-2.8.8/debian/patches/0021-Fix-regexp-to-support-new-GitHub-installation-tokens.patch
--- composer-2.8.8/debian/patches/0021-Fix-regexp-to-support-new-GitHub-installation-tokens.patch 1970-01-01 01:00:00.000000000 +0100
+++ composer-2.8.8/debian/patches/0021-Fix-regexp-to-support-new-GitHub-installation-tokens.patch 2026-05-14 10:37:03.000000000 +0200
@@ -0,0 +1,148 @@
+From: Philipp Scheit <[email protected]>
+Date: Wed, 13 May 2026 09:00:52 +0200
+Subject: Fix regexp to support new GitHub installation tokens format (#12853)
+
+Origin: upstream, https://github.com/composer/composer/commit/37872360dc9c0f8befc12f741e98db2aa9b1827c
+Bug: https://github.com/composer/composer/security/advisories/GHSA-f9f8-rm49-7jv2
+---
+ 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 55ac166..9650c96 100644
+--- a/src/Composer/IO/BaseIO.php
++++ b/src/Composer/IO/BaseIO.php
+@@ -136,9 +136,10 @@ abstract class BaseIO implements IOInterface
+
+ // 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 c6ad90f..c1cfdfe 100644
+--- a/src/Composer/Util/GitHub.php
++++ b/src/Composer/Util/GitHub.php
+@@ -23,7 +23,7 @@ use Composer\Pcre\Preg;
+ */
+ 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 0000000..b899d86
+--- /dev/null
++++ b/tests/Composer/Test/IO/BaseIOTest.php
+@@ -0,0 +1,99 @@
++<?php declare(strict_types=1);
++
++/*
++ * This file is part of Composer.
++ *
++ * (c) Nils Adermann <[email protected]>
++ * Jordi Boggiano <[email protected]>
++ *
++ * 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<string, array{string}> */
++ 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_<id>_<base64url>.<base64url>.<base64url>
++ // 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<string, array{string, string}> */
++ public static function provideUrlBreakingGithubTokens(): array
++ {
++ return [
++ 'contains @ (userinfo separator)' => ['[email protected]', '@'],
++ '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'],
++ ];
++ }
++}
diff -Nru composer-2.8.8/debian/patches/0022-Modernize-PHPUnit-syntax.patch composer-2.8.8/debian/patches/0022-Modernize-PHPUnit-syntax.patch
--- composer-2.8.8/debian/patches/0022-Modernize-PHPUnit-syntax.patch 1970-01-01 01:00:00.000000000 +0100
+++ composer-2.8.8/debian/patches/0022-Modernize-PHPUnit-syntax.patch 2026-05-14 10:37:03.000000000 +0200
@@ -0,0 +1,38 @@
+From: =?utf-8?q?David_Pr=C3=A9vot?= <[email protected]>
+Date: Fri, 29 Aug 2025 08:31:05 +0200
+Subject: Modernize PHPUnit syntax
+
+---
+ tests/Composer/Test/IO/BaseIOTest.php | 9 +++------
+ 1 file changed, 3 insertions(+), 6 deletions(-)
+
+diff --git a/tests/Composer/Test/IO/BaseIOTest.php b/tests/Composer/Test/IO/BaseIOTest.php
+index b899d86..e22d8ea 100644
+--- a/tests/Composer/Test/IO/BaseIOTest.php
++++ b/tests/Composer/Test/IO/BaseIOTest.php
+@@ -15,12 +15,11 @@ namespace Composer\Test\IO;
+ use Composer\Config;
+ use Composer\IO\BufferIO;
+ use Composer\Test\TestCase;
++use PHPUnit\Framework\Attributes\DataProvider;
+
+ class BaseIOTest extends TestCase
+ {
+- /**
+- * @dataProvider provideValidGithubTokens
+- */
++ #[DataProvider('provideValidGithubTokens')]
+ public function testLoadConfigurationAcceptsValidGithubToken(string $token): void
+ {
+ $io = new BufferIO();
+@@ -55,9 +54,7 @@ class BaseIOTest extends TestCase
+ ];
+ }
+
+- /**
+- * @dataProvider provideUrlBreakingGithubTokens
+- */
++ #[DataProvider('provideUrlBreakingGithubTokens')]
+ public function testLoadConfigurationRejectsTokenWithUrlBreakingCharacters(string $token, string $offending): void
+ {
+ $io = new BufferIO();
diff -Nru composer-2.8.8/debian/patches/series composer-2.8.8/debian/patches/series
--- composer-2.8.8/debian/patches/series 2026-04-15 10:50:08.000000000 +0200
+++ composer-2.8.8/debian/patches/series 2026-05-14 10:37:03.000000000 +0200
@@ -17,3 +17,6 @@
0017-Merge-commit-from-fork.patch
0018-Merge-commit-from-fork.patch
0019-Merge-commit-from-fork.patch
+0020-ProcessExecutor-mask-GitHub-fine-grained-access-toke.patch
+0021-Fix-regexp-to-support-new-GitHub-installation-tokens.patch
+0022-Modernize-PHPUnit-syntax.patch
signature.asc
Description: PGP signature
--- End Message ---