CVE Patch Review

CVE-2026-59948: Root-Cause Validation Added to Composer Lock Resolution

CVE-2026-59948 · Updated 2026-07-21 Root-cause

Summary

Composer patched dependency resolution by reapplying security-sensitive package metadata validation before resolved packages are written to or installed from the lock file. The change addresses the trust-boundary failure behind path traversal in bin entries and argument injection via source/dist metadata, and introduces a dedicated SecurityException for fail-closed handling.

Analysis

Vulnerability

CVE-2026-59948 describes a trust-boundary failure in Composer dependency resolution: metadata from untrusted repositories could reach lock-file generation and later installation without equivalent validation, enabling path traversal in package bin definitions, arbitrary file writes outside the project directory, and argument injection through source/dist fields. The patch comments in commit 502c6c4 and commit c50b1ef make the exploit mechanics explicit: a .. segment in bin can escape the package install directory, and source/dist values beginning with - can be interpreted as command-line options by VCS or download tooling.

The root cause is not a single unsafe sink but inconsistent validation coverage. Composer already had security checks in the array-loading path, but resolved packages could be loaded via a non-validating path and still be persisted to or consumed from the lock file. That gap meant attacker-controlled repository metadata could bypass earlier checks and survive until install-time side effects occurred.

// Re-applies the security-sensitive subset of the load() validation to a resolved package
// which may have been loaded via the non-validating ArrayLoader, before it is written to or
// installed from the lock file.
ValidatingArrayLoader::validatePackage($package);

Reference context is also available from the CVE record.

Patch

The patch adds a dedicated Composer\Exception\SecurityException and introduces ValidatingArrayLoader::validatePackage(PackageInterface $package), a static validator that re-applies the security-sensitive subset of package validation during dependency resolution. LockTransaction.php now invokes this validator on packages before they are written to or installed from the lock file, closing the gap between repository ingestion and lock/install workflows as shown in 502c6c4 and c50b1ef.

The new validation covers three security-relevant classes of metadata:

  • Package naming validation via hasPackageNamingError(), preventing structurally invalid or malicious names from surviving resolution.
  • Argument-injection protection for source.url, source.reference, dist.url, and dist.reference by rejecting values that begin with optional whitespace followed by -.
  • Path traversal protection for package binaries by rejecting any binary path containing a .. path segment across slash or backslash separators.
public static function validatePackage(PackageInterface $package): void
{
    if ($package instanceof RootPackageInterface) {
        return;
    }

    if (null !== ($err = self::hasPackageNamingError($package->getName()))) {
        throw new SecurityException('Invalid package found during dependency resolution, aborting: '.$err);
    }

    $sourceDist = [
        'source.url' => $package->getSourceUrl(),
        'source.reference' => $package->getSourceReference(),
        'dist.url' => $package->getDistUrl(),
        'dist.reference' => $package->getDistReference(),
    ];
    foreach ($sourceDist as $field => $value) {
        if ($value !== null && Preg::isMatch('{^\s*-}', $value)) {
            throw new SecurityException($package->getName().' has an invalid '.$field.', it must not start with a "-": '.$value);
        }
    }

    foreach ($package->getBinaries() as $bin) {
        if (Preg::isMatch('{(?:^|[\\/])\.\.(?:[\\/]|$)}', $bin)) {
            throw new SecurityException($package->getName().' has an invalid bin '.$bin.', it must not contain ".." path segments');
        }
    }
}

The test changes also normalize package names from bare single-segment values like A and foo to canonical Composer-style names like a/a and foo/foo. This is consistent with the stricter package-name validation and avoids false failures in solver tests once name validation is enforced during resolution.

Review

Pros

  • The fix is applied at the correct trust boundary: resolved packages are validated immediately before lock-file persistence or install consumption, which directly addresses the bypass condition described in the patch comments and the CVE summary.
  • The patch is fail-closed. Invalid metadata now raises a dedicated SecurityException instead of being tolerated until a later filesystem or process-execution stage.
  • The validation logic is security-focused and source-grounded: package names, source/dist option-like values, and binary traversal are all explicitly checked in one reusable method.
  • The binary path regex handles both Unix and Windows separators, which is important for a cross-platform package manager.
  • Updating tests to canonical package names indicates the maintainers accounted for compatibility between stricter validation and existing solver fixtures.

Cons

  • The patch only shows revalidation in LockTransaction. Based on the provided diff, confidence is high for lock-resolution coverage, but not absolute for every other code path that may materialize packages from untrusted metadata outside this flow.
  • The validator re-applies only a documented “security-sensitive subset” of load() validation. That is appropriate for this CVE, but it means future security-relevant fields could still drift if new metadata sinks are introduced and not added here.
  • The provided snippets do not show dedicated regression tests for malicious bin traversal or leading-dash source/dist values; the visible test changes are mostly fixture normalization. The implementation looks sound, but exploit-specific tests would improve long-term assurance.

Verdict

Root-cause.

This patch addresses the underlying issue: untrusted repository metadata was not consistently validated when packages passed through dependency resolution and lock-file handling. By moving security validation into the resolved-package path and aborting on malicious names, option-like source/dist values, and traversal-capable binary paths, Composer closes the validation gap rather than merely filtering one symptom. The remaining concern is completeness across all package-materialization paths, but within the supplied sources the remediation is technically coherent and appropriately placed.

Sources: commit 502c6c4f699802d9cf464728b3e8a95674f919a0, commit c50b1efd13ebd73f6dca19b31424c5a02bf93cc1, NVD entry, CVE record.

Sources