undefect. CWE-407 — 63 sites patched across 27 ecosystems

Authors: russell@unturf.com · brackishbert@gmail.com · foxhop.net · TimeHexOn.com

Patches, unit tests, benchmarks, whitepaper, and outreach briefs.
Public domain — no copyright claimed. Use freely.
This commit is contained in:
russell@unturf.com 2026-03-26 17:11:57 -04:00
commit 0a580b313d
70422 changed files with 17213626 additions and 0 deletions

View file

@ -0,0 +1,44 @@
diff --git a/src/Composer/Repository/RepositoryUtils.php b/src/Composer/Repository/RepositoryUtils.php
index e6960c6..9538b7b 100644
--- a/src/Composer/Repository/RepositoryUtils.php
+++ b/src/Composer/Repository/RepositoryUtils.php
@@ -34,6 +34,26 @@ class RepositoryUtils
* @return list<T>
*/
public static function filterRequiredPackages(array $packages, PackageInterface $requirer, bool $includeRequireDev = false, array $bucket = []): array
+ {
+ // CWE-407 fix: use SplObjectStorage (hash-backed set) for O(1) membership
+ // instead of in_array() which is O(|bucket|). SplObjectStorage is passed as
+ // an object reference so recursive calls share the same set instance.
+ $bucketSet = new \SplObjectStorage();
+ foreach ($bucket as $item) {
+ $bucketSet->attach($item);
+ }
+
+ return self::filterRequiredPackagesInternal($packages, $requirer, $includeRequireDev, $bucket, $bucketSet);
+ }
+
+ /**
+ * @template T of PackageInterface
+ * @param array<T> $packages
+ * @param list<T> $bucket
+ * @param \SplObjectStorage<T, null> $bucketSet
+ * @return list<T>
+ */
+ private static function filterRequiredPackagesInternal(array $packages, PackageInterface $requirer, bool $includeRequireDev, array $bucket, \SplObjectStorage $bucketSet): array
{
$requires = $requirer->getRequires();
if ($includeRequireDev) {
@@ -43,9 +63,10 @@ public static function filterRequiredPackages(array $packages, PackageInterface
foreach ($packages as $candidate) {
foreach ($candidate->getNames() as $name) {
if (isset($requires[$name])) {
- if (!in_array($candidate, $bucket, true)) {
+ if (!$bucketSet->contains($candidate)) {
+ $bucketSet->attach($candidate);
$bucket[] = $candidate;
- $bucket = self::filterRequiredPackages($packages, $candidate, false, $bucket);
+ $bucket = self::filterRequiredPackagesInternal($packages, $candidate, false, $bucket, $bucketSet);
}
break;
}

View file

@ -0,0 +1,67 @@
diff --git a/src/Composer/Repository/InstalledRepository.php b/src/Composer/Repository/InstalledRepository.php
index 3520fde..3cbe917 100644
--- a/src/Composer/Repository/InstalledRepository.php
+++ b/src/Composer/Repository/InstalledRepository.php
@@ -86,7 +86,7 @@ public function findPackagesWithReplacersAndProviders(string $name, $constraint
* @return array[] An associative array of arrays as described above.
* @phpstan-return array<array{0: PackageInterface, 1: Link, 2: array<mixed>|false}>
*/
- public function getDependents($needle, ?ConstraintInterface $constraint = null, bool $invert = false, bool $recurse = true, ?array $packagesFound = null): array
+ public function getDependents($needle, ?ConstraintInterface $constraint = null, bool $invert = false, bool $recurse = true, ?array $packagesFound = null, ?array $packagesFoundSet = null): array
{
$needles = array_map('strtolower', (array) $needle);
$results = [];
@@ -96,6 +96,13 @@ public function getDependents($needle, ?ConstraintInterface $constraint = null,
$packagesFound = $needles;
}
+ // CWE-407 fix: build a parallel hash set for O(1) in_array() replacement.
+ // $packagesFoundSet mirrors $packagesFound as an associative array keyed by
+ // package name string so isset() is O(1) instead of in_array() O(n).
+ if (null === $packagesFoundSet) {
+ $packagesFoundSet = array_fill_keys($packagesFound, true);
+ }
+
// locate root package for use below
$rootPackage = null;
foreach ($this->getPackages() as $package) {
@@ -112,6 +119,7 @@ public function getDependents($needle, ?ConstraintInterface $constraint = null,
// each loop needs its own "tree" as we want to show the complete dependent set of every needle
// without warning all the time about finding circular deps
$packagesInTree = $packagesFound;
+ $packagesInTreeSet = $packagesFoundSet;
// Replacements are considered valid reasons for a package to be installed during forward resolution
if (!$invert) {
@@ -125,12 +133,13 @@ public function getDependents($needle, ?ConstraintInterface $constraint = null,
if ($link->getSource() === $needle) {
if ($constraint === null || ($link->getConstraint()->matches($constraint) === true)) {
// already displayed this node's dependencies, cutting short
- if (in_array($link->getTarget(), $packagesInTree)) {
+ if (isset($packagesInTreeSet[$link->getTarget()])) {
$results[] = [$package, $link, false];
continue;
}
+ $packagesInTreeSet[$link->getTarget()] = true;
$packagesInTree[] = $link->getTarget();
- $dependents = $recurse ? $this->getDependents($link->getTarget(), null, false, true, $packagesInTree) : [];
+ $dependents = $recurse ? $this->getDependents($link->getTarget(), null, false, true, $packagesInTree, $packagesInTreeSet) : [];
$results[] = [$package, $link, $dependents];
$needles[] = $link->getTarget();
}
@@ -151,12 +160,13 @@ public function getDependents($needle, ?ConstraintInterface $constraint = null,
if ($link->getTarget() === $needle) {
if ($constraint === null || ($link->getConstraint()->matches($constraint) === !$invert)) {
// already displayed this node's dependencies, cutting short
- if (in_array($link->getSource(), $packagesInTree)) {
+ if (isset($packagesInTreeSet[$link->getSource()])) {
$results[] = [$package, $link, false];
continue;
}
+ $packagesInTreeSet[$link->getSource()] = true;
$packagesInTree[] = $link->getSource();
- $dependents = $recurse ? $this->getDependents($link->getSource(), null, false, true, $packagesInTree) : [];
+ $dependents = $recurse ? $this->getDependents($link->getSource(), null, false, true, $packagesInTree, $packagesInTreeSet) : [];
$results[] = [$package, $link, $dependents];
}
}