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 fix CVE-2025-67746 in a
point release rather than a DSA since it mostly fixes a display issue.

[ 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

Regards,

taffit
diff -Nru composer-2.8.8/debian/changelog composer-2.8.8/debian/changelog
--- composer-2.8.8/debian/changelog	2025-04-05 11:50:09.000000000 +0200
+++ composer-2.8.8/debian/changelog	2025-12-30 16:35:23.000000000 +0100
@@ -1,3 +1,11 @@
+composer (2.8.8-1+deb13u1) trixie; urgency=medium
+
+  * Backport fix from composer 2.9.3:
+    Fixed ANSI sequence injection [CVE-2025-67746]
+  * Track debian/trixie
+
+ -- David Prévot <[email protected]>  Tue, 30 Dec 2025 16:35:23 +0100
+
 composer (2.8.8-1) unstable; urgency=medium
 
   [ Jordi Boggiano ]
diff -Nru composer-2.8.8/debian/gbp.conf composer-2.8.8/debian/gbp.conf
--- composer-2.8.8/debian/gbp.conf	2024-06-25 07:54:44.000000000 +0200
+++ composer-2.8.8/debian/gbp.conf	2025-12-30 16:35:07.000000000 +0100
@@ -1,5 +1,5 @@
 [DEFAULT]
-debian-branch = debian/latest
+debian-branch = debian/trixie
 filter = [ '.gitattributes' ]
 pristine-tar = True
 upstream-vcs-tag = %(version%~%-)s
diff -Nru composer-2.8.8/debian/patches/0017-Merge-commit-from-fork.patch composer-2.8.8/debian/patches/0017-Merge-commit-from-fork.patch
--- composer-2.8.8/debian/patches/0017-Merge-commit-from-fork.patch	1970-01-01 01:00:00.000000000 +0100
+++ composer-2.8.8/debian/patches/0017-Merge-commit-from-fork.patch	2025-12-30 16:33:32.000000000 +0100
@@ -0,0 +1,355 @@
+From: Jordi Boggiano <[email protected]>
+Date: Tue, 30 Dec 2025 13:18:16 +0100
+Subject: Merge commit from fork
+
+Origin: upstream, https://github.com/composer/composer/commit/5db1876a76fdef76d3c4f8a27995c434c7a43e71
+Bug: https://github.com/composer/composer/security/advisories/GHSA-59pp-r3rg-353g
+Bug-Debian: https://security-tracker.debian.org/tracker/CVE-2025-67746
+---
+ src/Composer/Advisory/Auditor.php        |   4 +-
+ src/Composer/IO/ConsoleIO.php            |  46 ++++++-
+ tests/Composer/Test/IO/ConsoleIOTest.php | 200 +++++++++++++++++++++++++++++++
+ 3 files changed, 243 insertions(+), 7 deletions(-)
+
+diff --git a/src/Composer/Advisory/Auditor.php b/src/Composer/Advisory/Auditor.php
+index 485b332..3759787 100644
+--- a/src/Composer/Advisory/Auditor.php
++++ b/src/Composer/Advisory/Auditor.php
+@@ -295,7 +295,7 @@ class Auditor
+                 $io->getTable()
+                     ->setHorizontal()
+                     ->setHeaders($headers)
+-                    ->addRow($row)
++                    ->addRow(ConsoleIO::sanitize($row))
+                     ->setColumnWidth(1, 80)
+                     ->setColumnMaxWidth(1, 80)
+                     ->render();
+@@ -368,7 +368,7 @@ class Auditor
+ 
+         foreach ($packages as $pkg) {
+             $replacement = $pkg->getReplacementPackage() !== null ? $pkg->getReplacementPackage() : 'none';
+-            $table->addRow([$this->getPackageNameWithLink($pkg), $replacement]);
++            $table->addRow(ConsoleIO::sanitize([$this->getPackageNameWithLink($pkg), $replacement]));
+         }
+ 
+         $table->render();
+diff --git a/src/Composer/IO/ConsoleIO.php b/src/Composer/IO/ConsoleIO.php
+index 8ecea42..9d180ca 100644
+--- a/src/Composer/IO/ConsoleIO.php
++++ b/src/Composer/IO/ConsoleIO.php
+@@ -12,6 +12,7 @@
+ 
+ namespace Composer\IO;
+ 
++use Composer\Pcre\Preg;
+ use Composer\Question\StrictConfirmationQuestion;
+ use Symfony\Component\Console\Helper\HelperSet;
+ use Symfony\Component\Console\Helper\ProgressBar;
+@@ -120,6 +121,8 @@ class ConsoleIO extends BaseIO
+      */
+     public function write($messages, bool $newline = true, int $verbosity = self::NORMAL)
+     {
++        $messages = self::sanitize($messages);
++
+         $this->doWrite($messages, $newline, false, $verbosity);
+     }
+ 
+@@ -128,6 +131,8 @@ class ConsoleIO extends BaseIO
+      */
+     public function writeError($messages, bool $newline = true, int $verbosity = self::NORMAL)
+     {
++        $messages = self::sanitize($messages);
++
+         $this->doWrite($messages, $newline, true, $verbosity);
+     }
+ 
+@@ -252,7 +257,7 @@ class ConsoleIO extends BaseIO
+     {
+         /** @var \Symfony\Component\Console\Helper\QuestionHelper $helper */
+         $helper = $this->helperSet->get('question');
+-        $question = new Question($question, $default);
++        $question = new Question(self::sanitize($question), is_string($default) ? self::sanitize($default) : $default);
+ 
+         return $helper->ask($this->input, $this->getErrorOutput(), $question);
+     }
+@@ -264,7 +269,7 @@ class ConsoleIO extends BaseIO
+     {
+         /** @var \Symfony\Component\Console\Helper\QuestionHelper $helper */
+         $helper = $this->helperSet->get('question');
+-        $question = new StrictConfirmationQuestion($question, $default);
++        $question = new StrictConfirmationQuestion(self::sanitize($question), is_string($default) ? self::sanitize($default) : $default);
+ 
+         return $helper->ask($this->input, $this->getErrorOutput(), $question);
+     }
+@@ -276,7 +281,7 @@ class ConsoleIO extends BaseIO
+     {
+         /** @var \Symfony\Component\Console\Helper\QuestionHelper $helper */
+         $helper = $this->helperSet->get('question');
+-        $question = new Question($question, $default);
++        $question = new Question(self::sanitize($question), is_string($default) ? self::sanitize($default) : $default);
+         $question->setValidator($validator);
+         $question->setMaxAttempts($attempts);
+ 
+@@ -290,7 +295,7 @@ class ConsoleIO extends BaseIO
+     {
+         /** @var \Symfony\Component\Console\Helper\QuestionHelper $helper */
+         $helper = $this->helperSet->get('question');
+-        $question = new Question($question);
++        $question = new Question(self::sanitize($question));
+         $question->setHidden(true);
+ 
+         return $helper->ask($this->input, $this->getErrorOutput(), $question);
+@@ -303,7 +308,7 @@ class ConsoleIO extends BaseIO
+     {
+         /** @var \Symfony\Component\Console\Helper\QuestionHelper $helper */
+         $helper = $this->helperSet->get('question');
+-        $question = new ChoiceQuestion($question, $choices, $default);
++        $question = new ChoiceQuestion(self::sanitize($question), self::sanitize($choices), is_string($default) ? self::sanitize($default) : $default);
+         $question->setMaxAttempts($attempts ?: null); // IOInterface requires false, and Question requires null or int
+         $question->setErrorMessage($errorMessage);
+         $question->setMultiselect($multiselect);
+@@ -342,4 +347,35 @@ class ConsoleIO extends BaseIO
+ 
+         return $this->output;
+     }
++
++    /**
++     * Sanitize string to remove control characters
++     *
++     * If $allowNewlines is true, \x0A (\n) and \x0D\x0A (\r\n) are let through. Single \r are still sanitized away to prevent overwriting whole lines.
++     *
++     * All other control chars (except NULL bytes) as well as ANSI escape sequences are removed.
++     *
++     * @param string|iterable<string> $messages
++     * @return string|array<string>
++     * @phpstan-return ($messages is string ? string : array<string>)
++     */
++    public static function sanitize($messages, bool $allowNewlines = true)
++    {
++        // Match ANSI escape sequences:
++        // - CSI (Control Sequence Introducer): ESC [ params intermediate final
++        // - OSC (Operating System Command): ESC ] ... ESC \ or BEL
++        // - Other ESC sequences: ESC followed by any character
++        $escapePattern = '\x1B\[[\x30-\x3F]*[\x20-\x2F]*[\x40-\x7E]|\x1B\].*?(?:\x1B\\\\|\x07)|\x1B.';
++        $pattern = $allowNewlines ? "{{$escapePattern}|[\x01-\x09\x0B\x0C\x0E-\x1A]|\r(?!\n)}u" : "{{$escapePattern}|[\x01-\x1A]}u";
++        if (is_string($messages)) {
++            return Preg::replace($pattern, '', $messages);
++        }
++
++        $sanitized = [];
++        foreach ($messages as $key => $message) {
++            $sanitized[$key] = Preg::replace($pattern, '', $message);
++        }
++
++        return $sanitized;
++    }
+ }
+diff --git a/tests/Composer/Test/IO/ConsoleIOTest.php b/tests/Composer/Test/IO/ConsoleIOTest.php
+index 8e0a829..cf38bf4 100644
+--- a/tests/Composer/Test/IO/ConsoleIOTest.php
++++ b/tests/Composer/Test/IO/ConsoleIOTest.php
+@@ -296,4 +296,204 @@ class ConsoleIOTest extends TestCase
+         self::assertTrue($consoleIO->hasAuthentication('repoName'));
+         self::assertFalse($consoleIO->hasAuthentication('repoName2'));
+     }
++
++    /**
++     * @dataProvider sanitizeProvider
++     * @param string|string[] $input
++     * @param string|string[] $expected
++     */
++    public function testSanitize($input, bool $allowNewlines, $expected): void
++    {
++        self::assertSame($expected, ConsoleIO::sanitize($input, $allowNewlines));
++    }
++
++    /**
++     * @return array<string, array{input: string|string[], allowNewlines: bool, expected: string|string[]}>
++     */
++    public static function sanitizeProvider(): array
++    {
++        return [
++            // String input with allowNewlines=true
++            'string with \n allowed' => [
++                'input' => "Hello\nWorld",
++                'allowNewlines' => true,
++                'expected' => "Hello\nWorld",
++            ],
++            'string with \r\n allowed' => [
++                'input' => "Hello\r\nWorld",
++                'allowNewlines' => true,
++                'expected' => "Hello\r\nWorld",
++            ],
++            'string with standalone \r removed' => [
++                'input' => "Hello\rWorld",
++                'allowNewlines' => true,
++                'expected' => "HelloWorld",
++            ],
++            'string with escape sequence removed' => [
++                'input' => "Hello\x1B[31mWorld",
++                'allowNewlines' => true,
++                'expected' => "HelloWorld",
++            ],
++            'string with control chars removed' => [
++                'input' => "Hello\x01\x08\x09World",
++                'allowNewlines' => true,
++                'expected' => "HelloWorld",
++            ],
++            'string with mixed control chars and newlines' => [
++                'input' => "Line1\n\x1B[32mLine2\x08\rLine3",
++                'allowNewlines' => true,
++                'expected' => "Line1\nLine2Line3",
++            ],
++            'string with null bytes are allowed' => [
++                'input' => "Hello\x00World",
++                'allowNewlines' => true,
++                'expected' => "Hello\x00World",
++            ],
++
++            // String input with allowNewlines=false
++            'string with \n removed' => [
++                'input' => "Hello\nWorld",
++                'allowNewlines' => false,
++                'expected' => "HelloWorld",
++            ],
++            'string with \r\n removed' => [
++                'input' => "Hello\r\nWorld",
++                'allowNewlines' => false,
++                'expected' => "HelloWorld",
++            ],
++            'string with escape sequence removed (no newlines)' => [
++                'input' => "Hello\x1B[31mWorld",
++                'allowNewlines' => false,
++                'expected' => "HelloWorld",
++            ],
++            'string with all control chars removed' => [
++                'input' => "Hello\x01\x08\x09\x0A\x0DWorld",
++                'allowNewlines' => false,
++                'expected' => "HelloWorld",
++            ],
++
++            // Array input with allowNewlines=true
++            'array with newlines allowed' => [
++                'input' => ["Hello\nWorld", "Foo\r\nBar"],
++                'allowNewlines' => true,
++                'expected' => ["Hello\nWorld", "Foo\r\nBar"],
++            ],
++            'array with control chars removed' => [
++                'input' => ["Hello\x1B[31mWorld", "Foo\x08Bar\r"],
++                'allowNewlines' => true,
++                'expected' => ["HelloWorld", "FooBar"],
++            ],
++
++            // Array input with allowNewlines=false
++            'array with newlines removed' => [
++                'input' => ["Hello\nWorld", "Foo\r\nBar"],
++                'allowNewlines' => false,
++                'expected' => ["HelloWorld", "FooBar"],
++            ],
++            'array with all control chars removed' => [
++                'input' => ["Test\x01\x0A", "Data\x1B[m\x0D"],
++                'allowNewlines' => false,
++                'expected' => ["Test", "Data"],
++            ],
++
++            // Edge cases
++            'empty string' => [
++                'input' => '',
++                'allowNewlines' => true,
++                'expected' => '',
++            ],
++            'empty array' => [
++                'input' => [],
++                'allowNewlines' => true,
++                'expected' => [],
++            ],
++            'string with no control chars' => [
++                'input' => 'Hello World',
++                'allowNewlines' => true,
++                'expected' => 'Hello World',
++            ],
++            'string with unicode' => [
++                'input' => "Hello 世界\nTest",
++                'allowNewlines' => true,
++                'expected' => "Hello 世界\nTest",
++            ],
++
++            // Various ANSI escape sequences
++            'CSI with multiple parameters' => [
++                'input' => "Text\x1B[1;31;40mColored\x1B[0mNormal",
++                'allowNewlines' => true,
++                'expected' => "TextColoredNormal",
++            ],
++            'CSI SGR reset' => [
++                'input' => "Before\x1B[mAfter",
++                'allowNewlines' => true,
++                'expected' => "BeforeAfter",
++            ],
++            'CSI cursor positioning' => [
++                'input' => "Line\x1B[2J\x1B[H\x1B[10;5HText",
++                'allowNewlines' => true,
++                'expected' => "LineText",
++            ],
++            'OSC with BEL terminator' => [
++                'input' => "Text\x1B]0;Window Title\x07More",
++                'allowNewlines' => true,
++                'expected' => "TextMore",
++            ],
++            'OSC with ST terminator' => [
++                'input' => "Text\x1B]2;Title\x1B\\More",
++                'allowNewlines' => true,
++                'expected' => "TextMore",
++            ],
++            'Simple ESC sequences' => [
++                'input' => "Text\x1B7Saved\x1B8Restored\x1BcReset",
++                'allowNewlines' => true,
++                'expected' => "TextSavedRestoredReset",
++            ],
++            'ESC D (Index)' => [
++                'input' => "Line1\x1BDLine2",
++                'allowNewlines' => true,
++                'expected' => "Line1Line2",
++            ],
++            'ESC E (Next Line)' => [
++                'input' => "Line1\x1BELine2",
++                'allowNewlines' => true,
++                'expected' => "Line1Line2",
++            ],
++            'ESC M (Reverse Index)' => [
++                'input' => "Text\x1BMMore",
++                'allowNewlines' => true,
++                'expected' => "TextMore",
++            ],
++            'ESC N (SS2) and ESC O (SS3)' => [
++                'input' => "Text\x1BNchar\x1BOanother",
++                'allowNewlines' => true,
++                'expected' => "Textcharanother",
++            ],
++            'Multiple escape sequences in sequence' => [
++                'input' => "\x1B[1m\x1B[31m\x1B[44mBold Red on Blue\x1B[0m",
++                'allowNewlines' => true,
++                'expected' => "Bold Red on Blue",
++            ],
++            'CSI with question mark (private mode)' => [
++                'input' => "Text\x1B[?25lHidden\x1B[?25hVisible",
++                'allowNewlines' => true,
++                'expected' => "TextHiddenVisible",
++            ],
++            'CSI erase sequences' => [
++                'input' => "Clear\x1B[2J\x1B[K\x1B[1KScreen",
++                'allowNewlines' => true,
++                'expected' => "ClearScreen",
++            ],
++            'Hyperlink OSC 8' => [
++                'input' => "Click \x1B]8;;https://example.com\x1B\\here\x1B]8;;\x1B\\ for link",
++                'allowNewlines' => true,
++                'expected' => "Click here for link",
++            ],
++            'Mixed content with complex sequences' => [
++                'input' => "\x1B[1;33mWarning:\x1B[0m File\x1B[31m not\x1B[0m found\n\x1B[2KRetrying...",
++                'allowNewlines' => true,
++                'expected' => "Warning: File not found\nRetrying...",
++            ],
++        ];
++    }
+ }
diff -Nru composer-2.8.8/debian/patches/series composer-2.8.8/debian/patches/series
--- composer-2.8.8/debian/patches/series	2025-04-05 11:50:09.000000000 +0200
+++ composer-2.8.8/debian/patches/series	2025-12-30 16:33:32.000000000 +0100
@@ -14,3 +14,4 @@
 0014-Revert-Add-workaround-for-InstalledVersion-to-ensure.patch
 0015-Revert-Fix-regression-from-12233-in-InstalledVersion.patch
 0016-Modernize-PHPUnit-syntax.patch
+0017-Merge-commit-from-fork.patch

Attachment: signature.asc
Description: PGP signature

Reply via email to