A name is not trust
Package registries resolve a name to an artifact, not a source. When the party behind a trusted name changes, the system inherits trust it cannot verify.
A package registry resolves a name and a version to an artifact. That is the entire function. When a build system asks npm for express at 4.18.2, or asks PyPI for requests at 2.31.0, the registry returns the bytes recorded under that coordinate. It does not inspect those bytes for intent. It does not compare them against what the same name produced last week. It performs a lookup and returns a reference, and the reference is treated as the thing itself. The system optimized for reference. It did not validate. This is not a defect in npm or PyPI or Maven Central. It is the documented behaviour of a namespace: a coordinate maps to content, and whoever controls the coordinate controls what that mapping returns.
Semantic versioning sits on top of this and formalizes the delegation. A version range like ^1.4.0 tells the resolver to accept any 1.x release at or above 1.4.0. The declared meaning is compatibility. The operational meaning is standing authorization. The build accepts content that does not yet exist, published by whoever holds the name at the moment the resolver runs. A lockfile narrows the window by pinning an exact version and often an integrity hash, but the hash only proves that the bytes match what was recorded when the lock was written. It proves the artifact is unchanged. It does not prove the artifact is what the name was originally trusted to mean.
So the registry does exactly what it was built to do, and it does it consistently. It answers the question it was asked. The question is always about location: what lives at this name, at this version. The question is never about correspondence: does what lives here still belong to the party that earned the trust attached to this name. No standard behaviour in the resolution path asks the second question, because the second question was assumed to be already answered by the first. The identity of the source was folded into the address, and the address became the object of trust.
The assumption underneath all of this is that a name is a stable proxy for a source, and that a source is a stable proxy for trustworthiness. When a maintainer publishes a package and it accumulates dependents, the ecosystem treats the accumulated trust as a property of the name. The name becomes shorthand. Downstream systems do not re-derive whether the code is safe on every install; they retrieve it because the name is already trusted, and they trust the name because it was trusted before. Trust here is not enforced at the moment of use. It is delegated backward in time, to a state that has already passed.
The model assumes trust is persistent and transferable. Persistent, because the trust earned by version 1.2.0 is presumed to still hold at 1.4.0, and to hold at whatever version publishes tomorrow under the same coordinate. Transferable, because the trust attached to the name is presumed to carry across every release the name will ever emit. The registry does not re-evaluate the relationship on each publish. Publishing rights are an account property. The account is authorized once, and every subsequent artifact inherits that authorization by virtue of arriving through the authorized channel. The assumption was that the declared identity behind a name remains the same identity that earned the trust, across all updates, indefinitely.
That assumption is not written down as a threat model. It is embedded in the mechanics. Integrity hashes verify that bytes did not change in transit or storage. Signatures, where they exist, verify that the artifact was published by the holder of a key. Neither mechanism verifies that the holder of the key at version 1.4.0 is the same party, in the same posture, with the same intent, as the party that earned the ecosystem’s trust at version 1.2.0. The system was designed to detect corruption of the artifact. It was not designed to detect substitution of the source behind a name that has already been trusted. Continuity of the coordinate is treated as continuity of the source. That equivalence is the load-bearing assumption, and nothing in the resolution path tests it.
What changed is not that attackers grew more capable, and not that any maintainer made a mistake. What changed is the validity of the assumption itself. A package name is not a fixed object. It is a coordinate whose controlling party can change: through account transfer, through a compromised publishing credential, through a maintainer handing off a project, through a dependency quietly re-pointed to a new owner. Each of these is a normal, supported operation in the lifecycle of a registry. None of them is a breach of the registry. But each one severs the link between the trust the name accumulated and the source now standing behind it, and the registry has no mechanism that notices the severance.
The system did not re-evaluate trust when the source behind the name changed. It inherited trust from a past state. The dependent projects that pulled the package continued to resolve the same coordinate, and the resolver continued to return whatever the current controller published, because from the resolver’s perspective nothing was different. The name was the same. The account was authorized. The version was newer, exactly as semantic versioning invited. The artifact hash matched the artifact that was actually served. Every check the system was designed to run passed, because none of those checks was ever measuring the thing that had actually changed. This is where things shifted: the artifact, the public registry entry itself, became the objective, because control of the entry is control of everything downstream that trusts the entry by reference.
The consequence is that the trust the ecosystem believes it holds and the trust the ecosystem actually holds have quietly diverged. Attackers do not need to compromise the build server or forge a signature to exploit this. They need to become the source behind a name that is already trusted, and then publish. The registry treats the new artifact the way it treated every prior one, because the registry only ever knew the reference. The trust was real once. It described a real relationship between a name and a source. That relationship no longer holds, and the system that depends on it has no way to observe that it no longer holds. If you cannot see it, you do not control it.
The failure is not an event inside the registry. It is the ordinary output of resolution. When a build runs npm install or pip install, the resolver reads the declared coordinate, contacts the registry, and receives the artifact recorded there. It compares the returned bytes against the integrity hash in the lockfile and confirms they match. It confirms the publishing account was authorized. Every one of these operations is a check on the artifact and the channel. None of them is a check on the source. The resolver validates that the bytes are the bytes it was promised. It never asks whether the party who made that promise is the party the name was trusted to represent. Integrity of content stood in for identity of source, and the substitution leaves no trace because both produce the same observable result: a valid artifact, correctly served, from an authorized account, at the requested coordinate.
What an observer sees from outside the system is a sequence of successful operations. The version incremented, as semantic versioning permits. The signature, where Sigstore or a comparable mechanism is present, verified against a valid key. The download completed. The postinstall script executed with the privileges of the build environment, because that is what postinstall scripts do. At no point did the system take an action it was not designed to take. There was no privilege escalation, no forged credential presented to the registry, no corrupted transport. The malicious artifact arrived through the front door the ecosystem built, and the front door did exactly what it does for every artifact. It opened. This is why the behaviour cannot be characterized as a bypass. Nothing was circumvented. The control path ran to completion and returned success.
The reference itself became the unit of trust, and reference is cheap to control. Content is expensive to evaluate. A downstream project cannot audit every transitive dependency on every install, so it does not. It resolves. The lockfile records that it resolved a specific artifact at a specific time, and on the next install it re-resolves the same coordinate and re-confirms the same hash, so the record appears stable. But the record only describes the last state the system observed. It carries no information about whether the source behind the coordinate is continuous with the source that was there when the trust was first extended. The registry answers location. The lockfile records location. The build consumes location. Correspondence between the name and the party behind it is never in the loop, so its failure produces no signal. The system did not miss the substitution. It was never watching for it.
Any system that resolves a reference to content, and trusts the content because the reference was trusted, has separated the moment of trust from the moment of use. Trust is established once, against a source. Use happens repeatedly, against a coordinate. As long as the coordinate and the source stay bound, the two are indistinguishable and the shortcut costs nothing. The pattern is execution based on reference rather than verification: the system acts on what a name currently points to, on the strength of what the name meant when the trust was granted. The binding between name and source is assumed to be permanent because the system has no operation that would notice it breaking.
The same mechanism operates in DNS. A domain name is a coordinate. Applications, CNAME records, hardcoded API endpoints, and pinned links all resolve a name to whatever the current controller of the zone points it at. Trust is attached to the name. The client treats api.example.com as a stable identity. But control of a name is transferable and revocable in exactly the way control of a package name is. When a subdomain’s CNAME still points at a decommissioned cloud endpoint, and a new tenant claims that endpoint, the name now resolves to infrastructure the original owner does not operate. Nothing in DNS is broken. The record is valid. Resolution succeeds. The client connects and extends to the new controller every trust it had attached to the name, because the client was never verifying who stood behind the name. It was resolving the name. This is subdomain takeover, and it is the registry problem in a different protocol: a coordinate whose controller changed while the trust attached to it did not.
In both cases the failure requires no exotic capability. The attacker does not defeat the resolution system. The attacker becomes the thing the resolution system returns. Whoever controls the coordinate at the moment of use inherits every trust that was ever delegated to it, because delegation ran backward in time to a state that has already passed and was never re-checked against the present. The reference is the same. The source is not. A system built to act on references has no vantage point from which that difference is visible.
The registry resolves the name once. It does not revalidate the source. Trust attached to a coordinate does not expire when the party behind the coordinate changes, because nothing in the resolution path is measuring the party. The trust was real once. It did not disappear. It moved. The control exists. The outcome does not.
Keep Reading
systems driftYour browser obeys someone else
Chrome disabling uBlock Origin was not a vendor choice to escape but a structure to see: software resolved by reference, executed without revalidating trust.
ActivityPubThe trending panel counts the tag, never reads it
Mastodon's trending engine counts references to a hashtag, not what it means. #ChickenAnything shows how systems resolve by reference and inherit stale trust.
systems driftPAN-OS remembers the verdict, forgets the reasoning
Firewall rules, AD groups, and JWTs keep executing stored references long after the reality they described has drifted. The system revalidates nothing.
Stay in the loop
New writing delivered when it's ready. No schedule, no spam.