Immutable Versions on Packagist

moebrowne1 pts0 comments

Immutable Versions on 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 behavior worth understanding, and a change we are making on top of it.<br>Today: Stable version metadata on Packagist.org is now immutable. Once a version has been published, the git reference it points to can no longer change. Deleted versions are no longer erased without a trace: they are marked with a reason and remain visible on the package page.<br>How Packagist packages and versions are published<br>To publish a package on Packagist.org, you submit the URL of a VCS repository, nearly always a GitHub repository. Packagist.org then follows that repository through webhooks: whenever a tag is created, it reads the composer.json at that tag and creates a version record for it. Branches are tracked too, as so-called dev versions, using the format dev-name for branches called name.<br>A tag such as v3.9.0 becomes the stable version 3.9.0, pointing at the exact git commit the tag referenced at the time (see detailed tag/branch/version docs). When you require monolog/monolog: ^3.9 in your composer.json and run composer update, Composer resolves that version through the Packagist.org metadata and pins the commit in your lock file:<br>"name": "monolog/monolog",<br>"version": "3.9.0",<br>"source": {<br>"type": "git",<br>"url": "https://github.com/Seldaek/monolog.git",<br>"reference": "8b5a6f3c1d2e4f5a6b7c8d9e0f1a2b3c4d5e6f7a"<br>},<br>"dist": {<br>"type": "zip",<br>"url": "https://api.github.com/repos/Seldaek/monolog/zipball/8b5a6f3c1d2e4f5a6b7c8d9e0f1a2b3c4d5e6f7a",<br>"reference": "8b5a6f3c1d2e4f5a6b7c8d9e0f1a2b3c4d5e6f7a"<br>}Everyone building on top of the lock file or the Packagist.org version metadata treats the pairing of a version number and a git reference as permanent. Lock files reproduce it on every install. Mirrors, including Private Packagist, cache the package archive under that reference. Security scanners and advisory databases record findings against a specific version and assume the code behind it never changes.<br>The problem: silent rewrites of published versions<br>Until now, Packagist.org would silently rewrite the metadata stored for a stable version whenever the reference in the source repository changed, for example when the corresponding git tag was deleted and re-created pointing at a different commit. The mechanism did not care about intent. If v3.9.0 pointed at commit A yesterday and points at commit B today, Packagist.org updated its metadata to B, and anyone running composer update from then on resolved 3.9.0 to different code than users who had run it before.<br>"Nobody will have installed it yet"<br>Rather than an attack, the most common way versions are modified are maintainers fixing their own mistakes.<br>You tag a release and minutes later realize something is wrong. A bug was not fully fixed, a commit did not make it into the release, or a last-minute issue makes the package uninstallable. The instinctive fix: delete the tag, push a corrected one under the same version number. It has only been a few minutes, so surely nobody has installed it yet.<br>The assumption that nobody installs a release in its first minutes has never been true. Automated systems react to the Packagist.org changes feed within seconds of a release. Mirrors fetch and cache the package archive under the original reference. Security scanners download and analyze the code. Users with automated dependency updates in CI may already have run composer update, installed the version, and committed the original reference to their lock file.<br>Once the tag has been replaced, two different variants of the same version number are in circulation, and telling them apart is extremely hard. A user debugging an issue cannot tell from the version number which variant they have installed. A mirror serves the first variant while Packagist.org metadata describes the second, so two colleagues installing "the same version" an hour apart can end up with different code. A scanner's analysis result refers to code that the version no longer points to. Every one of these is a confusing problem that lands on a package user's desk, hours or days after the retag that seemed harmless.<br>There is an infinite supply of version numbers. If a release goes out wrong, tag the fix as the next patch version. That is the entire solution, it takes a minute, costs nothing, and every tool and every user handles the result correctly.<br>Retags as an attack vector<br>The same mechanism that trips up well-meaning maintainers is exactly what attacker’s need. Someone who gains push access to a repository, through a compromised maintainer account or a stolen token (the same vector behind several recent PHP ecosystem incidents), can move an existing, trusted, widely installed tag onto malicious code. Packagist.org would follow the change without comment, and everyone resolving that version afterwards would receive the...

version packagist reference versions composer release

Related Articles