Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package php-composer2 for openSUSE:Factory checked in at 2026-08-28 19:54:53 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/php-composer2 (Old) and /work/SRC/openSUSE:Factory/.php-composer2.new.1265 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "php-composer2" Fri Aug 28 19:54:53 2026 rev:39 rq:1374350 version:2.10.3 Changes: -------- --- /work/SRC/openSUSE:Factory/php-composer2/php-composer2.changes 2026-08-01 18:37:08.211955509 +0200 +++ /work/SRC/openSUSE:Factory/.php-composer2.new.1265/php-composer2.changes 2026-08-28 19:57:52.511983767 +0200 @@ -1,0 +2,16 @@ +Fri Aug 28 11:02:52 UTC 2026 - Petr Gajdos <[email protected]> + +- version update to 2.10.3 + * Validate package bin paths against path traversal using symlinks (GHSA-96h3-5x6v-m776, CVE-2026-59944) + * Fixed command injection via malicious Perforce url (GHSA-rvx4-ffvw-m9q3) + * Sanitize URL-embedded usernames/token in a few more places (#13044) + * Fixed matching of gitlab URLs to avoid possible credential leak to the wrong domain (#12988) + * Fixed PHP 8.6 deprecation warnings (#12967, #13028) + * Fixed error output when a policy blocks a package version to be clearer (#12993) + * Fixed the lock file's content-hash with a conflict marker ending up breaking at runtime (#13048) + * Fixed possible race condition while creating directories like the cache dir when running multiple + Composer processes in parallel (#12977) + * Fixed forgejo support to handle empty repositories better (#12968) + * Fixed FilterListApiClient not forwarding transport options (#13040) + +------------------------------------------------------------------- Old: ---- composer-2.10.2.tar.gz New: ---- composer-2.10.3.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ php-composer2.spec ++++++ --- /var/tmp/diff_new_pack.EetwlI/_old 2026-08-28 19:57:53.702025373 +0200 +++ /var/tmp/diff_new_pack.EetwlI/_new 2026-08-28 19:57:53.703025408 +0200 @@ -17,7 +17,7 @@ Name: php-composer2 -Version: 2.10.2 +Version: 2.10.3 Release: 0 Summary: Dependency Management for PHP License: MIT ++++++ composer-2.10.2.tar.gz -> composer-2.10.3.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/composer-2.10.2/composer.json new/composer-2.10.3/composer.json --- old/composer-2.10.2/composer.json 2026-07-01 11:24:45.000000000 +0200 +++ new/composer-2.10.3/composer.json 2026-08-27 13:34:23.000000000 +0200 @@ -23,6 +23,8 @@ ], "require": { "php": "^7.2.5 || ^8.0", + "ext-filter": "*", + "ext-hash": "*", "ext-json": "*", "composer/ca-bundle": "^1.5", "composer/class-map-generator": "^1.4.0", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/composer-2.10.2/composer.lock new/composer-2.10.3/composer.lock --- old/composer-2.10.2/composer.lock 2026-07-01 11:24:45.000000000 +0200 +++ new/composer-2.10.3/composer.lock 2026-08-27 13:34:23.000000000 +0200 @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "449b675e25bbd33266e405fe9ba8fe9b", + "content-hash": "c476757337544dcb83275f61d8692ed0", "packages": [ { "name": "composer/ca-bundle", @@ -2564,6 +2564,8 @@ "prefer-lowest": false, "platform": { "php": "^7.2.5 || ^8.0", + "ext-filter": "*", + "ext-hash": "*", "ext-json": "*" }, "platform-dev": {}, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/composer-2.10.2/src/Composer/Advisory/Auditor.php new/composer-2.10.3/src/Composer/Advisory/Auditor.php --- old/composer-2.10.2/src/Composer/Advisory/Auditor.php 2026-07-01 11:24:45.000000000 +0200 +++ new/composer-2.10.3/src/Composer/Advisory/Auditor.php 2026-08-27 13:34:23.000000000 +0200 @@ -523,6 +523,9 @@ if ($entry->url !== null) { $parts[] = 'URL: ' . $entry->url; } + if ($entry->source !== null) { + $parts[] = 'Source: ' . $entry->source; + } $io->write(implode('. ', $parts) . '.'); } } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/composer-2.10.2/src/Composer/Autoload/AutoloadGenerator.php new/composer-2.10.3/src/Composer/Autoload/AutoloadGenerator.php --- old/composer-2.10.2/src/Composer/Autoload/AutoloadGenerator.php 2026-07-01 11:24:45.000000000 +0200 +++ new/composer-2.10.3/src/Composer/Autoload/AutoloadGenerator.php 2026-08-27 13:34:23.000000000 +0200 @@ -426,7 +426,10 @@ } if (null === $suffix) { - $suffix = $locker !== null && $locker->isLocked() ? $locker->getLockData()['content-hash'] : bin2hex(random_bytes(16)); + // a lock file with an unresolved merge conflict has its content-hash replaced by a + // human readable message, which would end up inside the autoloader class names + $contentHash = $locker !== null && $locker->isLocked() ? $locker->getLockData()['content-hash'] : null; + $suffix = is_string($contentHash) && Preg::isMatch('{^[a-f0-9]+$}', $contentHash) ? $contentHash : bin2hex(random_bytes(16)); } } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/composer-2.10.2/src/Composer/Composer.php new/composer-2.10.3/src/Composer/Composer.php --- old/composer-2.10.2/src/Composer/Composer.php 2026-07-01 11:24:45.000000000 +0200 +++ new/composer-2.10.3/src/Composer/Composer.php 2026-08-27 13:34:23.000000000 +0200 @@ -51,9 +51,9 @@ * * @see getVersion() */ - public const VERSION = '2.10.2'; + public const VERSION = '2.10.3'; public const BRANCH_ALIAS_VERSION = ''; - public const RELEASE_DATE = '2026-07-01 11:24:45'; + public const RELEASE_DATE = '2026-08-27 13:34:23'; public const SOURCE_VERSION = ''; /** diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/composer-2.10.2/src/Composer/DependencyResolver/LockTransaction.php new/composer-2.10.3/src/Composer/DependencyResolver/LockTransaction.php --- old/composer-2.10.2/src/Composer/DependencyResolver/LockTransaction.php 2026-07-01 11:24:45.000000000 +0200 +++ new/composer-2.10.3/src/Composer/DependencyResolver/LockTransaction.php 2026-08-27 13:34:23.000000000 +0200 @@ -27,9 +27,9 @@ /** * packages in current lock file, platform repo or otherwise present * - * Indexed by spl_object_hash + * Indexed by spl_object_id * - * @var array<string, BasePackage> + * @var array<int, BasePackage> */ protected $presentMap; @@ -48,7 +48,7 @@ protected $resultPackages; /** - * @param array<string, BasePackage> $presentMap + * @param array<int, BasePackage> $presentMap * @param array<int, BasePackage> $unlockableMap */ public function __construct(Pool $pool, array $presentMap, array $unlockableMap, Decisions $decisions) @@ -112,7 +112,7 @@ } // if we're just updating mirrors we need to reset everything to the same as currently "present" packages' references to keep the lock file as-is - if ($updateMirrors === true && !array_key_exists(spl_object_hash($package), $this->presentMap)) { + if ($updateMirrors === true && !array_key_exists(spl_object_id($package), $this->presentMap)) { $package = $this->updateMirrorAndUrls($package); } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/composer-2.10.2/src/Composer/DependencyResolver/Pool.php new/composer-2.10.3/src/Composer/DependencyResolver/Pool.php --- old/composer-2.10.2/src/Composer/DependencyResolver/Pool.php 2026-07-01 11:24:45.000000000 +0200 +++ new/composer-2.10.3/src/Composer/DependencyResolver/Pool.php 2026-08-27 13:34:23.000000000 +0200 @@ -41,7 +41,7 @@ protected $unacceptableFixedOrLockedPackages; /** @var array<string, array<string, string>> Map of package name => normalized version => pretty version */ protected $removedVersions = []; - /** @var array<string, array<string, string>> Map of package object hash => removed normalized versions => removed pretty version */ + /** @var array<int, array<string, string>> Map of package object id => removed normalized versions => removed pretty version */ protected $removedVersionsByPackage = []; /** @var array<string, array<string, array<SecurityAdvisory|PartialSecurityAdvisory>>> Map of package name => normalized version => security advisories */ private $securityRemovedVersions = []; @@ -54,7 +54,7 @@ * @param BasePackage[] $packages * @param BasePackage[] $unacceptableFixedOrLockedPackages * @param array<string, array<string, string>> $removedVersions - * @param array<string, array<string, string>> $removedVersionsByPackage + * @param array<int, array<string, string>> $removedVersionsByPackage * @param array<string, array<string, array<SecurityAdvisory|PartialSecurityAdvisory>>> $securityRemovedVersions * @param array<string, array<string, string>> $abandonedRemovedVersions * @param array<string, array<string, list<FilterListEntry>>> $filterListRemovedVersions @@ -101,17 +101,17 @@ /** * @return array<string, string> */ - public function getRemovedVersionsByPackage(string $objectHash): array + public function getRemovedVersionsByPackage(int $objectId): array { - if (!isset($this->removedVersionsByPackage[$objectHash])) { + if (!isset($this->removedVersionsByPackage[$objectId])) { return []; } - return $this->removedVersionsByPackage[$objectHash]; + return $this->removedVersionsByPackage[$objectId]; } /** - * @return array<string, array<string, string>> + * @return array<int, array<string, string>> */ public function getAllRemovedVersionsByPackage(): array { @@ -201,17 +201,18 @@ foreach ($this->filterListRemovedVersions[$packageName] ?? [] as $version => $filterListEntries) { if ($constraint !== null && $constraint->matches(new Constraint('==', $version))) { foreach ($filterListEntries as $entry) { - $entryKey = spl_object_hash($entry); + $entryKey = spl_object_id($entry); if (isset($seen[$entryKey])) { continue; } $seen[$entryKey] = true; + $source = (bool) $entry->source ? ' reported by ' . $entry->source : ''; $url = (bool) $entry->url ? ' (see ' . $entry->url . ')' : ''; $reason = (bool) $entry->reason ? ' reason: ' . $entry->reason : ''; - $lists[$entry->listName][] = $url . $reason; + $lists[$entry->listName][] = $source . $url . $reason; } } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/composer-2.10.2/src/Composer/DependencyResolver/PoolBuilder.php new/composer-2.10.3/src/Composer/DependencyResolver/PoolBuilder.php --- old/composer-2.10.2/src/Composer/DependencyResolver/PoolBuilder.php 2026-07-01 11:24:45.000000000 +0200 +++ new/composer-2.10.3/src/Composer/DependencyResolver/PoolBuilder.php 2026-08-27 13:34:23.000000000 +0200 @@ -78,7 +78,7 @@ private $io; /** * @var array[] - * @phpstan-var array<string, AliasPackage[]> + * @phpstan-var array<int, AliasPackage[]> */ private $aliasMap = []; /** @@ -303,8 +303,8 @@ $constraint = $this->temporaryConstraints[$packageName]; $packageAndAliases = [$i => $package]; - if (isset($this->aliasMap[spl_object_hash($package)])) { - $packageAndAliases += $this->aliasMap[spl_object_hash($package)]; + if (isset($this->aliasMap[spl_object_id($package)])) { + $packageAndAliases += $this->aliasMap[spl_object_id($package)]; } $found = false; @@ -478,7 +478,7 @@ $this->packages[$index] = $package; if ($package instanceof AliasPackage) { - $this->aliasMap[spl_object_hash($package->getAliasOf())][$index] = $package; + $this->aliasMap[spl_object_id($package->getAliasOf())][$index] = $package; } $name = $package->getName(); @@ -514,7 +514,7 @@ $newIndex = $this->indexCounter++; $this->packages[$newIndex] = $aliasPackage; - $this->aliasMap[spl_object_hash($aliasPackage->getAliasOf())][$newIndex] = $aliasPackage; + $this->aliasMap[spl_object_id($aliasPackage->getAliasOf())][$newIndex] = $aliasPackage; } foreach ($package->getRequires() as $link) { @@ -780,12 +780,12 @@ unset($this->loadedPerRepo[$repoIndex][$package->getName()][$package->getVersion()]); unset($this->packages[$index]); - if (isset($this->aliasMap[spl_object_hash($package)])) { - foreach ($this->aliasMap[spl_object_hash($package)] as $aliasIndex => $aliasPackage) { + if (isset($this->aliasMap[spl_object_id($package)])) { + foreach ($this->aliasMap[spl_object_id($package)] as $aliasIndex => $aliasPackage) { unset($this->loadedPerRepo[$repoIndex][$aliasPackage->getName()][$aliasPackage->getVersion()]); unset($this->packages[$aliasIndex]); } - unset($this->aliasMap[spl_object_hash($package)]); + unset($this->aliasMap[spl_object_id($package)]); } } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/composer-2.10.2/src/Composer/DependencyResolver/PoolOptimizer.php new/composer-2.10.3/src/Composer/DependencyResolver/PoolOptimizer.php --- old/composer-2.10.2/src/Composer/DependencyResolver/PoolOptimizer.php 2026-07-01 11:24:45.000000000 +0200 +++ new/composer-2.10.3/src/Composer/DependencyResolver/PoolOptimizer.php 2026-08-27 13:34:23.000000000 +0200 @@ -59,7 +59,7 @@ private $aliasesPerPackage = []; /** - * @var array<string, array<string, string>> + * @var array<int, array<string, string>> */ private $removedVersionsByPackage = []; @@ -364,7 +364,7 @@ } foreach ($versions as $version => $prettyVersion) { - $this->removedVersionsByPackage[spl_object_hash($package)][$version] = $prettyVersion; + $this->removedVersionsByPackage[spl_object_id($package)][$version] = $prettyVersion; } } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/composer-2.10.2/src/Composer/DependencyResolver/Problem.php new/composer-2.10.3/src/Composer/DependencyResolver/Problem.php --- old/composer-2.10.2/src/Composer/DependencyResolver/Problem.php 2026-07-01 11:24:45.000000000 +0200 +++ new/composer-2.10.3/src/Composer/DependencyResolver/Problem.php 2026-08-27 13:34:23.000000000 +0200 @@ -38,7 +38,7 @@ { /** * A map containing the id of each rule part of this problem as a key - * @var array<string, true> + * @var array<int, true> */ protected $reasonSeen; @@ -58,7 +58,7 @@ */ public function addRule(Rule $rule): void { - $this->addReason(spl_object_hash($rule), $rule); + $this->addReason(spl_object_id($rule), $rule); } /** @@ -182,7 +182,7 @@ $messages[] = $template; $templates[$template][$m[1]][$parser->normalize($m[2])] = $m[2]; $sourcePackage = $rule->getSourcePackage($pool); - foreach ($pool->getRemovedVersionsByPackage(spl_object_hash($sourcePackage)) as $version => $prettyVersion) { + foreach ($pool->getRemovedVersionsByPackage(spl_object_id($sourcePackage)) as $version => $prettyVersion) { $templates[$template][$m[1]][$version] = $prettyVersion; } } elseif ($message !== '') { @@ -230,10 +230,10 @@ /** * Store a reason descriptor but ignore duplicates * - * @param string $id A canonical identifier for the reason - * @param Rule $reason The reason descriptor + * @param int $id A canonical identifier for the reason + * @param Rule $reason The reason descriptor */ - protected function addReason(string $id, Rule $reason): void + protected function addReason(int $id, Rule $reason): void { // TODO: if a rule is part of a problem description in two sections, isn't this going to remove a message // that is important to understand the issue? @@ -553,7 +553,7 @@ } } if ($pool !== null && $useRemovedVersionGroup) { - foreach ($pool->getRemovedVersionsByPackage(spl_object_hash($package)) as $version => $prettyVersion) { + foreach ($pool->getRemovedVersionsByPackage(spl_object_id($package)) as $version => $prettyVersion) { $prepared[$package->getName()]['versions'][$version] = $prettyVersion; } } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/composer-2.10.2/src/Composer/DependencyResolver/Request.php new/composer-2.10.3/src/Composer/DependencyResolver/Request.php --- old/composer-2.10.2/src/Composer/DependencyResolver/Request.php 2026-07-01 11:24:45.000000000 +0200 +++ new/composer-2.10.3/src/Composer/DependencyResolver/Request.php 2026-08-27 13:34:23.000000000 +0200 @@ -44,11 +44,11 @@ protected $lockedRepository; /** @var array<string, ConstraintInterface> */ protected $requires = []; - /** @var array<string, BasePackage> */ + /** @var array<int, BasePackage> */ protected $fixedPackages = []; - /** @var array<string, BasePackage> */ + /** @var array<int, BasePackage> */ protected $lockedPackages = []; - /** @var array<string, BasePackage> */ + /** @var array<int, BasePackage> */ protected $fixedLockedPackages = []; /** @var array<string> */ protected $updateAllowList = []; @@ -83,7 +83,7 @@ */ public function fixPackage(BasePackage $package): void { - $this->fixedPackages[spl_object_hash($package)] = $package; + $this->fixedPackages[spl_object_id($package)] = $package; } /** @@ -98,7 +98,7 @@ */ public function lockPackage(BasePackage $package): void { - $this->lockedPackages[spl_object_hash($package)] = $package; + $this->lockedPackages[spl_object_id($package)] = $package; } /** @@ -110,13 +110,13 @@ */ public function fixLockedPackage(BasePackage $package): void { - $this->fixedPackages[spl_object_hash($package)] = $package; - $this->fixedLockedPackages[spl_object_hash($package)] = $package; + $this->fixedPackages[spl_object_id($package)] = $package; + $this->fixedLockedPackages[spl_object_id($package)] = $package; } public function unlockPackage(BasePackage $package): void { - unset($this->lockedPackages[spl_object_hash($package)]); + unset($this->lockedPackages[spl_object_id($package)]); } /** @@ -156,7 +156,7 @@ } /** - * @return array<string, BasePackage> + * @return array<int, BasePackage> */ public function getFixedPackages(): array { @@ -165,11 +165,11 @@ public function isFixedPackage(BasePackage $package): bool { - return isset($this->fixedPackages[spl_object_hash($package)]); + return isset($this->fixedPackages[spl_object_id($package)]); } /** - * @return array<string, BasePackage> + * @return array<int, BasePackage> */ public function getLockedPackages(): array { @@ -178,11 +178,11 @@ public function isLockedPackage(PackageInterface $package): bool { - return isset($this->lockedPackages[spl_object_hash($package)]) || isset($this->fixedLockedPackages[spl_object_hash($package)]); + return isset($this->lockedPackages[spl_object_id($package)]) || isset($this->fixedLockedPackages[spl_object_id($package)]); } /** - * @return array<string, BasePackage> + * @return array<int, BasePackage> */ public function getFixedOrLockedPackages(): array { @@ -190,7 +190,7 @@ } /** - * @return ($packageIds is true ? array<int, BasePackage> : array<string, BasePackage>) + * @return array<int, BasePackage> * * @TODO look into removing the packageIds option, the only place true is used * is for the installed map in the solver problems. @@ -203,12 +203,12 @@ if ($this->lockedRepository !== null) { foreach ($this->lockedRepository->getPackages() as $package) { - $presentMap[$packageIds ? $package->getId() : spl_object_hash($package)] = $package; + $presentMap[$packageIds ? $package->getId() : spl_object_id($package)] = $package; } } foreach ($this->fixedPackages as $package) { - $presentMap[$packageIds ? $package->getId() : spl_object_hash($package)] = $package; + $presentMap[$packageIds ? $package->getId() : spl_object_id($package)] = $package; } return $presentMap; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/composer-2.10.2/src/Composer/DependencyResolver/Solver.php new/composer-2.10.3/src/Composer/DependencyResolver/Solver.php --- old/composer-2.10.2/src/Composer/DependencyResolver/Solver.php 2026-07-01 11:24:45.000000000 +0200 +++ new/composer-2.10.3/src/Composer/DependencyResolver/Solver.php 2026-08-27 13:34:23.000000000 +0200 @@ -50,7 +50,7 @@ protected $problems = []; /** @var array<Rule[]> */ protected $learnedPool = []; - /** @var array<string, int> */ + /** @var array<int, int> */ protected $learnedWhy = []; /** @var bool */ @@ -340,7 +340,7 @@ $this->rules->add($newRule, RuleSet::TYPE_LEARNED); - $this->learnedWhy[spl_object_hash($newRule)] = $why; + $this->learnedWhy[spl_object_id($newRule)] = $why; $ruleNode = new RuleWatchNode($newRule); $ruleNode->watch2OnHighest($this->decisions); @@ -518,11 +518,11 @@ } /** - * @param array<string, true> $ruleSeen + * @param array<int, true> $ruleSeen */ private function analyzeUnsolvableRule(Problem $problem, Rule $conflictRule, array &$ruleSeen): void { - $why = spl_object_hash($conflictRule); + $why = spl_object_id($conflictRule); $ruleSeen[$why] = true; if ($conflictRule->getType() === RuleSet::TYPE_LEARNED) { @@ -530,7 +530,7 @@ $problemRules = $this->learnedPool[$learnedWhy]; foreach ($problemRules as $problemRule) { - if (!isset($ruleSeen[spl_object_hash($problemRule)])) { + if (!isset($ruleSeen[spl_object_id($problemRule)])) { $this->analyzeUnsolvableRule($problem, $problemRule, $ruleSeen); } } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/composer-2.10.2/src/Composer/DependencyResolver/Transaction.php new/composer-2.10.3/src/Composer/DependencyResolver/Transaction.php --- old/composer-2.10.2/src/Composer/DependencyResolver/Transaction.php 2026-07-01 11:24:45.000000000 +0200 +++ new/composer-2.10.3/src/Composer/DependencyResolver/Transaction.php 2026-08-27 13:34:23.000000000 +0200 @@ -38,7 +38,7 @@ /** * Package set resulting from this transaction - * @var array<string, PackageInterface> + * @var array<int, PackageInterface> */ protected $resultPackageMap; @@ -87,7 +87,7 @@ $this->resultPackageMap = []; foreach ($resultPackages as $package) { - $this->resultPackageMap[spl_object_hash($package)] = $package; + $this->resultPackageMap[spl_object_id($package)] = $package; foreach ($package->getNames() as $name) { $this->resultPackagesByName[$name][] = $package; } @@ -128,12 +128,12 @@ while (\count($stack) > 0) { $package = array_pop($stack); - if (isset($processed[spl_object_hash($package)])) { + if (isset($processed[spl_object_id($package)])) { continue; } - if (!isset($visited[spl_object_hash($package)])) { - $visited[spl_object_hash($package)] = true; + if (!isset($visited[spl_object_id($package)])) { + $visited[spl_object_id($package)] = true; $stack[] = $package; if ($package instanceof AliasPackage) { @@ -147,8 +147,8 @@ } } } - } elseif (!isset($processed[spl_object_hash($package)])) { - $processed[spl_object_hash($package)] = true; + } elseif (!isset($processed[spl_object_id($package)])) { + $processed[spl_object_id($package)] = true; if ($package instanceof AliasPackage) { $aliasKey = $package->getName().'::'.$package->getVersion(); @@ -225,7 +225,7 @@ * These serve as a starting point to enumerate packages in a topological order despite potential cycles. * If there are packages with a cycle on the top level the package with the lowest name gets picked * - * @return array<string, PackageInterface> + * @return array<int, PackageInterface> */ protected function getRootPackages(): array { @@ -241,7 +241,7 @@ foreach ($possibleRequires as $require) { if ($require !== $package) { - unset($roots[spl_object_hash($require)]); + unset($roots[spl_object_id($require)]); } } } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/composer-2.10.2/src/Composer/Downloader/FileDownloader.php new/composer-2.10.3/src/Composer/Downloader/FileDownloader.php --- old/composer-2.10.2/src/Composer/Downloader/FileDownloader.php 2026-07-01 11:24:45.000000000 +0200 +++ new/composer-2.10.3/src/Composer/Downloader/FileDownloader.php 2026-08-27 13:34:23.000000000 +0200 @@ -26,6 +26,7 @@ use Composer\Plugin\PostFileDownloadEvent; use Composer\Plugin\PreFileDownloadEvent; use Composer\EventDispatcher\EventDispatcher; +use Composer\Installer\BinaryInstaller; use Composer\Util\Filesystem; use Composer\Util\Http\Response; use Composer\Util\Platform; @@ -364,9 +365,16 @@ // Single files can not have a mode set like files in archives // so we make sure if the file is a binary that it is executable foreach ($package->getBinaries() as $bin) { - if (file_exists($path . '/' . $bin) && !is_executable($path . '/' . $bin)) { - Silencer::call('chmod', $path . '/' . $bin, 0777 & ~umask()); + $binPath = $path . '/' . $bin; + if (!file_exists($binPath) || is_executable($binPath)) { + continue; + } + // a bin resolving outside of the package would let it chmod an arbitrary host file, this + // is reported by BinaryInstaller later in the same install (GHSA-96h3-5x6v-m776) + if (!BinaryInstaller::isBinPathInsidePackage($path, $binPath)) { + continue; } + Silencer::call('chmod', $binPath, 0777 & ~umask()); } return \React\Promise\resolve(null); @@ -448,7 +456,7 @@ $extension = $package->getDistType(); } - return rtrim($this->config->get('vendor-dir') . '/composer/tmp-' . hash('md5', $package . spl_object_hash($package)) . '.' . $extension, '.'); + return rtrim($this->config->get('vendor-dir') . '/composer/tmp-' . hash('md5', $package . spl_object_id($package)) . '.' . $extension, '.'); } /** diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/composer-2.10.2/src/Composer/EventDispatcher/EventDispatcher.php new/composer-2.10.3/src/Composer/EventDispatcher/EventDispatcher.php --- old/composer-2.10.2/src/Composer/EventDispatcher/EventDispatcher.php 2026-07-01 11:24:45.000000000 +0200 +++ new/composer-2.10.3/src/Composer/EventDispatcher/EventDispatcher.php 2026-08-27 13:34:23.000000000 +0200 @@ -707,10 +707,10 @@ return 'fn:'.$cb; } if (is_object($cb)) { - return 'obj:'.spl_object_hash($cb); + return 'obj:'.spl_object_id($cb); } if (is_array($cb)) { - return 'array:'.(is_string($cb[0]) ? $cb[0] : get_class($cb[0]) .'#'.spl_object_hash($cb[0])).'::'.$cb[1]; + return 'array:'.(is_string($cb[0]) ? $cb[0] : get_class($cb[0]) .'#'.spl_object_id($cb[0])).'::'.$cb[1]; } // not great but also do not want to break everything here diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/composer-2.10.2/src/Composer/FilterList/FilterListApiClient.php new/composer-2.10.3/src/Composer/FilterList/FilterListApiClient.php --- old/composer-2.10.2/src/Composer/FilterList/FilterListApiClient.php 2026-07-01 11:24:45.000000000 +0200 +++ new/composer-2.10.3/src/Composer/FilterList/FilterListApiClient.php 2026-08-27 13:34:23.000000000 +0200 @@ -24,10 +24,16 @@ { /** @var HttpDownloader */ private $httpDownloader; + /** @var mixed[] */ + private $options; - public function __construct(HttpDownloader $httpDownloader) + /** + * @param mixed[] $options Stream context options e.g. https://www.php.net/manual/en/context.http.php + */ + public function __construct(HttpDownloader $httpDownloader, array $options = []) { $this->httpDownloader = $httpDownloader; + $this->options = $options; } /** @@ -47,8 +53,11 @@ 'lists' => $configuredLists, ]; - $options = []; + $options = $this->options; $options['http']['method'] = 'POST'; + if (isset($options['http']['header'])) { + $options['http']['header'] = (array) $options['http']['header']; + } $options['http']['header'][] = 'Content-type: application/json'; $options['http']['timeout'] = 10; $options['http']['content'] = json_encode($body); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/composer-2.10.2/src/Composer/Installer/BinaryInstaller.php new/composer-2.10.3/src/Composer/Installer/BinaryInstaller.php --- old/composer-2.10.2/src/Composer/Installer/BinaryInstaller.php 2026-07-01 11:24:45.000000000 +0200 +++ new/composer-2.10.3/src/Composer/Installer/BinaryInstaller.php 2026-08-27 13:34:23.000000000 +0200 @@ -68,6 +68,13 @@ $this->io->writeError(' <warning>Skipped installation of bin '.$bin.' for package '.$package->getName().': found a directory at that path</warning>'); continue; } + // A malicious package can pass the ".." bin metadata check yet ship the bin as a symlink + // pointing outside the package (e.g. to ../../../victim.sh), following it here would let + // the package chmod/proxy an arbitrary host file (GHSA-96h3-5x6v-m776). + if (!self::isBinPathInsidePackage($installPath, $binPath)) { + $this->io->writeError(' <warning>Skipped installation of bin '.$bin.' for package '.$package->getName().': the bin resolves to a path outside of the package directory</warning>'); + continue; + } if (!$this->filesystem->isAbsolutePath($binPath)) { // in case a custom installer returned a relative path for the // $package, we can now safely turn it into a absolute path (as we @@ -145,6 +152,26 @@ } /** + * Checks that a bin file resolves to a path inside the package's own install directory + * + * A bin escaping the package, either via ".." metadata or by being a symlink pointing out of it, + * would let the package chmod/proxy an arbitrary host file, see GHSA-gjfg-22fp-rrxx and + * GHSA-96h3-5x6v-m776. + */ + public static function isBinPathInsidePackage(string $installPath, string $binPath): bool + { + $realBinPath = realpath($binPath); + $realInstallPath = realpath($installPath); + + // fail closed if either path cannot be resolved + if (false === $realBinPath || false === $realInstallPath) { + return false; + } + + return strpos($realBinPath, $realInstallPath.DIRECTORY_SEPARATOR) === 0; + } + + /** * @return string[] */ protected function getBinaries(PackageInterface $package): array diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/composer-2.10.2/src/Composer/Package/Loader/ValidatingArrayLoader.php new/composer-2.10.3/src/Composer/Package/Loader/ValidatingArrayLoader.php --- old/composer-2.10.2/src/Composer/Package/Loader/ValidatingArrayLoader.php 2026-07-01 11:24:45.000000000 +0200 +++ new/composer-2.10.3/src/Composer/Package/Loader/ValidatingArrayLoader.php 2026-08-27 13:34:23.000000000 +0200 @@ -23,6 +23,7 @@ use Composer\Semver\Constraint\MatchNoneConstraint; use Composer\Semver\Intervals; use Composer\Spdx\SpdxLicenses; +use Composer\Util\Perforce; /** * @author Jordi Boggiano <[email protected]> @@ -551,6 +552,14 @@ if (isset($this->config[$srcType]['url']) && Preg::isMatch('{^\s*-}', (string) $this->config[$srcType]['url'])) { $this->errors[] = $srcType . '.url : must not start with a "-", "'.$this->config[$srcType]['url'].'" given'; } + // a perforce url is passed to the p4 client as P4PORT, where rsh:/jsh: endpoints + // mean "run this command locally" (GHSA-rvx4-ffvw-m9q3) + if ($srcType === 'source' && ($this->config[$srcType]['type'] ?? null) === 'perforce' + && isset($this->config[$srcType]['url']) && is_string($this->config[$srcType]['url']) + && !Perforce::isValidPort($this->config[$srcType]['url']) + ) { + $this->errors[] = $srcType . '.url : invalid Perforce port ("'.$this->config[$srcType]['url'].'"), it must be of the form [tcp|ssl:][host:]port'; + } } } @@ -700,6 +709,14 @@ } } + // A perforce source.url ends up as the p4 client's P4PORT, and a "rsh:"/"jsh:" endpoint + // there makes the client execute the rest of the value as a local command instead of + // connecting to a server (GHSA-rvx4-ffvw-m9q3), so only accept network endpoints. + $sourceUrl = $package->getSourceUrl(); + if ($package->getSourceType() === 'perforce' && $sourceUrl !== null && !Perforce::isValidPort($sourceUrl)) { + throw new SecurityException($package->getName().' has an invalid source.url, it must be a Perforce port of the form [tcp|ssl:][host:]port: '.$sourceUrl); + } + // Bin paths are resolved relative to the package install dir and then chmod'd (and // proxied) by BinaryInstaller. A ".." segment escapes that directory and lets a // dependency chmod/point at an arbitrary host file (GHSA-gjfg-22fp-rrxx), so reject it. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/composer-2.10.2/src/Composer/Repository/ArrayRepository.php new/composer-2.10.3/src/Composer/Repository/ArrayRepository.php --- old/composer-2.10.2/src/Composer/Repository/ArrayRepository.php 2026-07-01 11:24:45.000000000 +0200 +++ new/composer-2.10.3/src/Composer/Repository/ArrayRepository.php 2026-08-27 13:34:23.000000000 +0200 @@ -71,10 +71,10 @@ && !isset($alreadyLoaded[$package->getName()][$package->getVersion()]) ) { // add selected packages which match stability requirements - $result[spl_object_hash($package)] = $package; + $result[spl_object_id($package)] = $package; // add the aliased package for packages where the alias matches - if ($package instanceof AliasPackage && !isset($result[spl_object_hash($package->getAliasOf())])) { - $result[spl_object_hash($package->getAliasOf())] = $package->getAliasOf(); + if ($package instanceof AliasPackage && !isset($result[spl_object_id($package->getAliasOf())])) { + $result[spl_object_id($package->getAliasOf())] = $package->getAliasOf(); } } @@ -85,8 +85,8 @@ // add aliases of packages that were selected, even if the aliases did not match foreach ($packages as $package) { if ($package instanceof AliasPackage) { - if (isset($result[spl_object_hash($package->getAliasOf())])) { - $result[spl_object_hash($package)] = $package; + if (isset($result[spl_object_id($package->getAliasOf())])) { + $result[spl_object_id($package)] = $package; } } } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/composer-2.10.2/src/Composer/Repository/ComposerRepository.php new/composer-2.10.3/src/Composer/Repository/ComposerRepository.php --- old/composer-2.10.2/src/Composer/Repository/ComposerRepository.php 2026-07-01 11:24:45.000000000 +0200 +++ new/composer-2.10.3/src/Composer/Repository/ComposerRepository.php 2026-08-27 13:34:23.000000000 +0200 @@ -573,9 +573,9 @@ $namesFound[$name] = true; if (!$constraint || $constraint->matches(new Constraint('==', $candidate->getVersion()))) { - $matches[spl_object_hash($candidate)] = $candidate; - if ($candidate instanceof AliasPackage && !isset($matches[spl_object_hash($candidate->getAliasOf())])) { - $matches[spl_object_hash($candidate->getAliasOf())] = $candidate->getAliasOf(); + $matches[spl_object_id($candidate)] = $candidate; + if ($candidate instanceof AliasPackage && !isset($matches[spl_object_id($candidate->getAliasOf())])) { + $matches[spl_object_id($candidate->getAliasOf())] = $candidate->getAliasOf(); } } } @@ -583,8 +583,8 @@ // add aliases of matched packages even if they did not match the constraint foreach ($candidates as $candidate) { if ($candidate instanceof AliasPackage) { - if (isset($matches[spl_object_hash($candidate->getAliasOf())])) { - $matches[spl_object_hash($candidate)] = $candidate; + if (isset($matches[spl_object_id($candidate->getAliasOf())])) { + $matches[spl_object_id($candidate)] = $candidate; } } } @@ -911,7 +911,7 @@ private function getFilterApiClient(): FilterListApiClient { if ($this->filterApiClient === null) { - $this->filterApiClient = new FilterListApiClient($this->httpDownloader); + $this->filterApiClient = new FilterListApiClient($this->httpDownloader, $this->options); } return $this->filterApiClient; @@ -1275,7 +1275,7 @@ * @phpstan-param array<string, BasePackage::STABILITY_*>|null $stabilityFlags * @param array<string, array<string, PackageInterface>> $alreadyLoaded * - * @return array{namesFound: array<string, true>, packages: array<string, BasePackage>} + * @return array{namesFound: array<string, true>, packages: array<int, BasePackage>} */ private function loadAsyncPackages(array $packageNames, ?array $acceptableStabilities = null, ?array $stabilityFlags = null, array $alreadyLoaded = []): array { @@ -1345,11 +1345,11 @@ $loadedPackages = $this->createPackages($versionsToLoad, $packagesSource); foreach ($loadedPackages as $package) { $package->setRepository($this); - $packages[spl_object_hash($package)] = $package; + $packages[spl_object_id($package)] = $package; - if ($package instanceof AliasPackage && !isset($packages[spl_object_hash($package->getAliasOf())])) { + if ($package instanceof AliasPackage && !isset($packages[spl_object_id($package->getAliasOf())])) { $package->getAliasOf()->setRepository($this); - $packages[spl_object_hash($package->getAliasOf())] = $package->getAliasOf(); + $packages[spl_object_id($package->getAliasOf())] = $package->getAliasOf(); } } }); @@ -1775,7 +1775,7 @@ } // TODO use scarier wording once we know for sure it doesn't do false positives anymore - throw new RepositorySecurityException('The contents of '.$filename.' do not match its signature. This could indicate a man-in-the-middle attack or e.g. antivirus software corrupting files. Try running composer again and report this if you think it is a mistake.'); + throw new RepositorySecurityException('The contents of '.Url::sanitize($filename).' do not match its signature. This could indicate a man-in-the-middle attack or e.g. antivirus software corrupting files. Try running composer again and report this if you think it is a mistake.'); } if ($this->eventDispatcher) { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/composer-2.10.2/src/Composer/Repository/PlatformRepository.php new/composer-2.10.3/src/Composer/Repository/PlatformRepository.php --- old/composer-2.10.2/src/Composer/Repository/PlatformRepository.php 2026-07-01 11:24:45.000000000 +0200 +++ new/composer-2.10.3/src/Composer/Repository/PlatformRepository.php 2026-08-27 13:34:23.000000000 +0200 @@ -234,7 +234,8 @@ $info = $this->runtime->getExtensionInfo($name); // SSL Version => OpenSSL/1.0.1t - if (Preg::isMatchStrictGroups('{^SSL Version => (?<library>[^/]+)/(?<version>.+)$}im', $info, $sslMatches)) { + // unversioned backends (=> Schannel) are not reported, the library must not span lines (#12615) + if (Preg::isMatchStrictGroups('{^SSL Version => (?<library>[^\r\n/]+)/(?<version>[^\r\n]+?)\r?$}im', $info, $sslMatches)) { $library = strtolower($sslMatches['library']); if ($library === 'openssl') { $parsedVersion = Version::parseOpenssl($sslMatches['version'], $isFips); @@ -252,7 +253,7 @@ } // libSSH Version => libssh2/1.4.3 - if (Preg::isMatchStrictGroups('{^libSSH Version => (?<library>[^/]+)/(?<version>.+?)(?:/.*)?$}im', $info, $sshMatches)) { + if (Preg::isMatchStrictGroups('{^libSSH Version => (?<library>[^\r\n/]+)/(?<version>.+?)(?:/.*)?$}im', $info, $sshMatches)) { $this->addLibrary($libraries, $name.'-'.strtolower($sshMatches['library']), $sshMatches['version'], 'curl '.$sshMatches['library'].' version'); } @@ -393,8 +394,8 @@ $this->addLibrary($libraries, $name.'-libmbfl', $libmbflMatches['version'], 'mbstring libmbfl version'); } - if ($this->runtime->hasConstant('MB_ONIGURUMA_VERSION')) { - $this->addLibrary($libraries, $name.'-oniguruma', $this->runtime->getConstant('MB_ONIGURUMA_VERSION'), 'mbstring oniguruma version'); + if (\PHP_VERSION_ID < 90000 && $this->runtime->hasConstant('MB_ONIGURUMA_VERSION')) { + $this->addLibrary($libraries, $name.'-oniguruma', Silencer::call([$this->runtime, 'getConstant'], 'MB_ONIGURUMA_VERSION'), 'mbstring oniguruma version'); // Multibyte regex (oniguruma) version => 5.9.5 // oniguruma version => 6.9.0 @@ -700,6 +701,12 @@ return; } + // a parsing glitch in one of the extension info parsers above must not result in a bogus + // package name, as that would then be looked up as if it was a real package, see #12615 + if (!self::isPlatformPackage('lib-'.$name)) { + return; + } + // avoid adding the same lib twice even if two conflicting extensions provide the same lib // see https://github.com/composer/composer/issues/12082 if (isset($libraries['lib-'.$name])) { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/composer-2.10.2/src/Composer/Repository/Vcs/ForgejoDriver.php new/composer-2.10.3/src/Composer/Repository/Vcs/ForgejoDriver.php --- old/composer-2.10.2/src/Composer/Repository/Vcs/ForgejoDriver.php 2026-07-01 11:24:45.000000000 +0200 +++ new/composer-2.10.3/src/Composer/Repository/Vcs/ForgejoDriver.php 2026-08-27 13:34:23.000000000 +0200 @@ -58,6 +58,10 @@ $resource = $this->forgejoUrl->apiUrl.'/contents/' . $file . '?ref='.urlencode($identifier); $resource = $this->getContents($resource)->decodeJson(); + if ($resource === []) { + return '[]'; + } + // The Forgejo contents API only returns files up to 1MB as base64 encoded files // larger files either need be fetched with a raw accept header or by using the git blob endpoint if ((!isset($resource['content']) || $resource['content'] === '') && $resource['encoding'] === 'none' && isset($resource['git_url'])) { @@ -105,6 +109,9 @@ do { $response = $this->getContents($resource); $branchData = $response->decodeJson(); + if ($branchData === null) { + break; + } foreach ($branchData as $branch) { $branches[$branch['name']] = $branch['commit']['id']; } @@ -130,6 +137,9 @@ do { $response = $this->getContents($resource); $tagsData = $response->decodeJson(); + if ($tagsData === null) { + break; + } foreach ($tagsData as $tag) { $tags[$tag['name']] = $tag['commit']['sha']; } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/composer-2.10.2/src/Composer/Util/Filesystem.php new/composer-2.10.3/src/Composer/Util/Filesystem.php --- old/composer-2.10.2/src/Composer/Util/Filesystem.php 2026-07-01 11:24:45.000000000 +0200 +++ new/composer-2.10.3/src/Composer/Util/Filesystem.php 2026-08-27 13:34:23.000000000 +0200 @@ -258,6 +258,12 @@ } if (!@mkdir($directory, 0777, true)) { + // maybe another process created it since we checked above? + clearstatcache(); + if (is_dir($directory)) { + return; + } + $e = new \RuntimeException($directory.' does not exist and could not be created: '.(error_get_last()['message'] ?? '')); // in pathological cases with paths like path/to/broken-symlink/../foo is_dir will fail to detect path/to/foo diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/composer-2.10.2/src/Composer/Util/Git.php new/composer-2.10.3/src/Composer/Util/Git.php --- old/composer-2.10.2/src/Composer/Util/Git.php 2026-07-01 11:24:45.000000000 +0200 +++ new/composer-2.10.3/src/Composer/Util/Git.php 2026-08-27 13:34:23.000000000 +0200 @@ -81,7 +81,7 @@ $callables[] = static function (string $url) use ($cmd): array { $map = [ '%url%' => $url, - '%sanitizedUrl%' => Preg::replace('{://([^@]+?):(.+?)@}', '://', $url), + '%sanitizedUrl%' => Url::stripCredentials($url), ]; return array_map(static function ($value) use ($map): string { @@ -573,10 +573,13 @@ $commands = [ ['git', 'remote', 'set-url', 'origin', '--', '%url%'], ['git', 'remote', 'show', 'origin'], - ['git', 'remote', 'set-url', 'origin', '--', '%sanitizedUrl%'], ]; - $this->runCommands($commands, $url, $dir, false, $output); + try { + $this->runCommands($commands, $url, $dir, false, $output); + } finally { + $this->runCommands([['git', 'remote', 'set-url', 'origin', '--', '%sanitizedUrl%']], $url, $dir); + } } $lines = $this->process->splitLines($output); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/composer-2.10.2/src/Composer/Util/Http/CurlDownloader.php new/composer-2.10.3/src/Composer/Util/Http/CurlDownloader.php --- old/composer-2.10.2/src/Composer/Util/Http/CurlDownloader.php 2026-07-01 11:24:45.000000000 +0200 +++ new/composer-2.10.3/src/Composer/Util/Http/CurlDownloader.php 2026-08-27 13:34:23.000000000 +0200 @@ -432,7 +432,7 @@ // Gzipped responses with missing Content-Length header cannot be detected during the file download // because $progress['size_download'] refers to the gzipped size downloaded, not the actual file size if ($contents !== false && Platform::strlen($contents) >= $maxFileSize) { - throw new MaxFileSizeExceededException('Maximum allowed download size reached. Downloaded ' . Platform::strlen($contents) . ' of allowed ' . $maxFileSize . ' bytes'); + throw new MaxFileSizeExceededException('Maximum allowed download size reached. Downloaded ' . Platform::strlen($contents) . ' of allowed ' . $maxFileSize . ' bytes for ' . Url::sanitize($job['url'])); } } else { $contents = stream_get_contents($job['bodyHandle']); @@ -518,12 +518,12 @@ if (isset($this->jobs[$i]['options']['max_file_size'])) { // Compare max_file_size with the content-length header this value will be -1 until the header is parsed if ($this->jobs[$i]['options']['max_file_size'] < $progress['download_content_length']) { - $this->rejectJob($this->jobs[$i], new MaxFileSizeExceededException('Maximum allowed download size reached. Content-length header indicates ' . $progress['download_content_length'] . ' bytes. Allowed ' . $this->jobs[$i]['options']['max_file_size'] . ' bytes')); + $this->rejectJob($this->jobs[$i], new MaxFileSizeExceededException('Maximum allowed download size reached. Content-length header indicates ' . $progress['download_content_length'] . ' bytes. Allowed ' . $this->jobs[$i]['options']['max_file_size'] . ' bytes for ' . Url::sanitize($this->jobs[$i]['url']))); } // Compare max_file_size with the download size in bytes if ($this->jobs[$i]['options']['max_file_size'] < $progress['size_download']) { - $this->rejectJob($this->jobs[$i], new MaxFileSizeExceededException('Maximum allowed download size reached. Downloaded ' . $progress['size_download'] . ' of allowed ' . $this->jobs[$i]['options']['max_file_size'] . ' bytes')); + $this->rejectJob($this->jobs[$i], new MaxFileSizeExceededException('Maximum allowed download size reached. Downloaded ' . $progress['size_download'] . ' of allowed ' . $this->jobs[$i]['options']['max_file_size'] . ' bytes for ' . Url::sanitize($this->jobs[$i]['url']))); } } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/composer-2.10.2/src/Composer/Util/Perforce.php new/composer-2.10.3/src/Composer/Util/Perforce.php --- old/composer-2.10.2/src/Composer/Util/Perforce.php 2026-07-01 11:24:45.000000000 +0200 +++ new/composer-2.10.3/src/Composer/Util/Perforce.php 2026-08-27 13:34:23.000000000 +0200 @@ -12,6 +12,7 @@ namespace Composer\Util; +use Composer\Exception\SecurityException; use Composer\IO\IOInterface; use Composer\Pcre\Preg; use Symfony\Component\Process\ExecutableFinder; @@ -64,6 +65,10 @@ */ public function __construct($repoConfig, string $port, string $path, ProcessExecutor $process, bool $isWindows, IOInterface $io) { + if (!self::isValidPort($port)) { + throw new SecurityException('Invalid Perforce port ('.$port.'), it must be of the form [tcp|ssl:][host:]port'); + } + $this->windowsFlag = $isWindows; $this->p4Port = $port; $this->initializePath($path); @@ -82,10 +87,34 @@ public static function checkServerExists(string $url, ProcessExecutor $processExecutor): bool { + // this is a detection probe which runs against every VCS repository url when no driver + // matched yet, so an unusable port is simply "not a perforce server" and not an error + if (!self::isValidPort($url)) { + return false; + } + return 0 === $processExecutor->execute(['p4', '-p', $url, 'info', '-s'], $ignoredOutput); } /** + * Checks that a P4PORT value is a network endpoint the p4 client can connect to. + * + * A `rsh:`/`jsh:` P4PORT makes the p4 client run the rest of the value as a local child process + * instead of connecting to a server, so a package-controlled source.url must never reach it + * (GHSA-rvx4-ffvw-m9q3). Only the documented `[transport:][host:]port` forms are accepted. + */ + public static function isValidPort(string $url): bool + { + // rsh/jsh are transport keywords to p4, so "rsh:foo" never parses as host "rsh" port "foo" + // and has to be rejected before the shape check below would happily accept it + if (Preg::isMatch('{^\s*+(?:rsh|jsh)\s*+:}i', $url)) { + return false; + } + + return Preg::isMatch('{^(?:(?:tcp|ssl)(?:4|6|46|64)?:)?(?:\[[0-9a-f:.]++\]|[a-z0-9._][a-z0-9._-]*+)(?::[a-z0-9._][a-z0-9._-]*+)?$}iD', $url); + } + + /** * @phpstan-param RepoConfig $repoConfig */ public function initialize($repoConfig): void diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/composer-2.10.2/src/Composer/Util/ProcessExecutor.php new/composer-2.10.3/src/Composer/Util/ProcessExecutor.php --- old/composer-2.10.2/src/Composer/Util/ProcessExecutor.php 2026-07-01 11:24:45.000000000 +0200 +++ new/composer-2.10.3/src/Composer/Util/ProcessExecutor.php 2026-08-27 13:34:23.000000000 +0200 @@ -482,14 +482,7 @@ } $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 { - $user = Url::sanitizeUsername($m['user']); - if (($m['password'] ?? '') !== '') { - return '://'.$user.':***@'; - } - - return '://'.$user.'@'; - }, $commandString); + $safeCommand = Url::sanitize($commandString); $safeCommand = Preg::replace("{--password (.*[^\\\\]\') }", '--password \'***\' ', $safeCommand); $this->io->writeError('Executing'.($async ? ' async' : '').' command ('.($cwd ?: 'CWD').'): '.$safeCommand); } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/composer-2.10.2/src/Composer/Util/RemoteFilesystem.php new/composer-2.10.3/src/Composer/Util/RemoteFilesystem.php --- old/composer-2.10.2/src/Composer/Util/RemoteFilesystem.php 2026-07-01 11:24:45.000000000 +0200 +++ new/composer-2.10.3/src/Composer/Util/RemoteFilesystem.php 2026-08-27 13:34:23.000000000 +0200 @@ -320,7 +320,7 @@ $contentLength = !empty($http_response_header[0]) ? Response::findHeaderValue($http_response_header, 'content-length') : null; if ($contentLength && Platform::strlen($result) < $contentLength) { // alas, this is not possible via the stream callback because STREAM_NOTIFY_COMPLETED is documented, but not implemented anywhere in PHP - $e = new TransportException('Content-Length mismatch, received '.Platform::strlen($result).' bytes out of the expected '.$contentLength); + $e = new TransportException('Content-Length mismatch, received '.Platform::strlen($result).' bytes out of the expected '.$contentLength.' for '.Url::sanitize($fileUrl)); $e->setHeaders($http_response_header); $e->setStatusCode(self::findStatusCode($http_response_header)); try { @@ -537,7 +537,7 @@ } if ($result !== false && $maxFileSize !== null && Platform::strlen($result) >= $maxFileSize) { - throw new MaxFileSizeExceededException('Maximum allowed download size reached. Downloaded ' . Platform::strlen($result) . ' of allowed ' . $maxFileSize . ' bytes'); + throw new MaxFileSizeExceededException('Maximum allowed download size reached. Downloaded ' . Platform::strlen($result) . ' of allowed ' . $maxFileSize . ' bytes for ' . Url::sanitize($fileUrl)); } // https://www.php.net/manual/en/reserved.variables.httpresponseheader.php diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/composer-2.10.2/src/Composer/Util/Url.php new/composer-2.10.3/src/Composer/Util/Url.php --- old/composer-2.10.2/src/Composer/Util/Url.php 2026-07-01 11:24:45.000000000 +0200 +++ new/composer-2.10.3/src/Composer/Util/Url.php 2026-08-27 13:34:23.000000000 +0200 @@ -103,7 +103,9 @@ && !in_array($origin, $config->get('gitlab-domains'), true) ) { foreach ($config->get('gitlab-domains') as $gitlabDomain) { - if ($gitlabDomain !== '' && str_starts_with($gitlabDomain, $origin)) { + // configured domains may spell out a port the URL omits, see GitLab::authorizeOAuth + $bcDomain = Preg::replace('{^([^/]+):\d+}', '$1', $gitlabDomain); + if ($gitlabDomain !== '' && ($bcDomain === $origin || str_starts_with($bcDomain, $origin.'/'))) { return $gitlabDomain; } } @@ -131,7 +133,9 @@ // e.g. https://api.github.com/repositories/9999999999?access_token=github_token $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 { + // matches every scheme://user:pass@ in the string as URLs are usually embedded in a longer + // message, plus a scheme-less one at the very start for URLs passed in on their own + $url = Preg::replaceCallback('{(?:(?P<prefix>[a-z0-9][a-z0-9+.-]*://)|\A)(?P<user>[^:/\s?#]*)(?::(?P<password>[^\s/?#]+))?@}i', static function ($m): string { $user = self::sanitizeUsername($m['user']); if (($m['password'] ?? '') !== '') { return $m['prefix'].$user.':***@'; @@ -144,6 +148,16 @@ } /** + * Removes the credentials from a URL entirely, for URLs which get persisted somewhere (e.g. in a git remote) + * + * Unlike sanitize() this also drops the username, as a bare token in the user slot is a credential too. + */ + public static function stripCredentials(string $url): string + { + return Preg::replace('{://[^/\s?#]+@}', '://', $url); + } + + /** * Returns a display-safe version of the user/token part of a URL's userinfo. * * Modern GitHub tokens (e.g. ghp_xxx, github_pat_xxx) and any other long (12char+) value keep ++++++ composer.phar ++++++ Binary files /var/tmp/diff_new_pack.EetwlI/_old and /var/tmp/diff_new_pack.EetwlI/_new differ ++++++ composer.phar.asc ++++++ Binary files /var/tmp/diff_new_pack.EetwlI/_old and /var/tmp/diff_new_pack.EetwlI/_new differ
