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

Attachment: signature.asc
Description: PGP signature

Reply via email to