Closing Composer's Download Fallback Paths in Private Packagist

orkj1 pts0 comments

Closing Composer's Download Fallback Paths in Private Packagist

This is the next post in our supply chain security series, following the supply chain security update and the Composer 2.10 release. Each post in this series covers a specific Composer behavior worth understanding, and a Private Packagist feature we are introducing on top of it.<br>Today: How Composer's package download fallback behavior, originally designed for resilience, can become a supply chain risk, and the new Private Packagist options that close it off.<br>Resilience through fallbacks<br>Composer's download model was designed to be resilient. If a mirror is unavailable, it tries to fall back to the original distribution download URL. If the dist download fails, it falls back to cloning from source. Each fallback gives the install a chance to succeed even when one part of the infrastructure is down.<br>For an organization using Private Packagist as a mirror of Packagist.org, this resulted in lock file entries that look like this:<br>"name": "psr/log",<br>"version": "3.0.2",<br>"source": {<br>"type": "git",<br>"url": "https://github.com/php-fig/log.git",<br>"reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3"<br>},<br>"dist": {<br>"type": "zip",<br>"url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",<br>"reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",<br>"mirrors": [<br>"url": "https://repo.packagist.com/my-org/dists/%package%/%version%/r%reference%.%type%",<br>"preferred": true<br>}Three different ways to obtain the same package are advertised: The preferred mirror URL on Private Packagist, the original dist URL on GitHub, and the git source repository. When everything is running as expected, Composer downloads from the preferred mirror and the other entries never get used.<br>Resilience that never materialized<br>In practice, this resilience was already mostly theatre for any organization with private packages of its own, which is most Private Packagist customers. The CI runners, deploy environments, and developer machines installing from a private repository typically only hold credentials for Private Packagist itself, not for the upstream VCS hosts like GitHub. When the preferred mirror URL fails, the fallback to the original dist URL fails too, because the upstream rejects an unauthenticated request, and the source clone fails for the same reason.<br>The same is true for packages defined directly on Private Packagist through artifact uploads or custom JSON package definitions: The original URL is a packagist.com URL to begin with, so if Private Packagist itself is unavailable, the fallback URLs are unavailable for the same reason. Private Packagist has exceeded five nines of availability for several years, so this fallback path is in practice never used anyway.<br>The fallback paths only ever quietly succeed for mirrored public third-party packages, the exact case where the supply chain risk lives.<br>Where resilience becomes a supply chain risk<br>Suppose a malicious version of psr/log is published. Private Packagist detects it (through the Aikido malware feed, organization-level allow-lists, or any other security policy you have configured) and refuses to serve that specific version by returning a 404 or another error code and an error message for its dist URL. The intent on the repository side is straightforward: Stop developers from downloading the compromised artifact.<br>What Composer actually does:<br>Package operations: 1 install, 0 updates, 0 removals<br>- Downloading psr/log (3.0.2)<br>Warning from repo.packagist.com:<br>This dist file has been flagged as malware<br>Failed to download psr/log from dist: The "https://repo.packagist.com/my-org/dists/psr/log/3.0.2.0/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3.zip" file could not be downloaded (HTTP/2 404 )<br>- Downloading psr/log (3.0.2)<br>- Installing psr/log (3.0.2): Extracting archiveComposer notes the failure, picks up the next URL in the lock file, and downloads the malicious artifact directly from GitHub. The decision made by Private Packagist to block the version is silently overridden by the fallback behavior built into the client.<br>If GitHub zip downloads are also unavailable for that specific tag, Composer falls one step further, to a source checkout. This is even more relevant when mirroring other third-party package repositories, where artifact and repository may not be hosted by the same service.<br>This dist file has been flagged as malware<br>Failed to download psr/log from dist: The<br>"https://codeload.github.com/php-fig/log/legacy.zip/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" file could not be downloaded (HTTP/2 404 )<br>Now trying to download from source<br>- Installing psr/log (3.0.2): Cloning f16e1d5863The developer's machine now clones directly from the attacker-controlled git repository, bypassing every infrastructure protection that was in place to block the artifact.<br>Composer 2.10: A partial solution<br>The Composer config options governing this behavior are preferred-install (whether to prefer dist or...

packagist private composer dist from download

Related Articles