The uncomfortable thing about software supply-chain attacks is how ordinary the opening move is. No zero-day, no exotic exploit. Someone with publish rights to a package you already depend on has their account taken over — usually through a phishing email good enough to fool a busy human — and everything downstream of that account is now attacker-controlled.
We’ve watched this exact shape play out publicly more than once: event-stream,
ua-parser-js, coa, rc, the node-ipc sabotage. Different payloads, same
anatomy. Worth walking through it slowly, because every link in the chain is a place
the attack could have been stopped — and almost none of them are where teams
actually look.
The chain
- A maintainer is phished. A widely-depended-on package has a maintainer whose npm (or PyPI, or crates.io) credentials get captured. Sometimes it’s a fake “your 2FA is expiring” page; sometimes it’s a malicious dependency in the maintainer’s own toolchain. Either way, publish rights change hands quietly.
- A new version ships. The attacker publishes a fresh patch release —
4.17.21→4.17.22— carrying a maliciouspostinstallscript, or a few obfuscated lines buried in a file nobody re-reviews between patch versions. - Floating ranges do the distribution for free. Half the ecosystem depends on
^4.17.0. Within minutes, CI pipelines resolving that range start pulling the poisoned version. No human chose to upgrade. The package manager did, exactly as designed. - The payload runs at install time.
postinstallexecutes on the CI runner — or the developer laptop — before anyone opens a pull request, reads a diff, or runs a test. By the time a scanner or a code review might catch it, the credential exfiltration or the crypto-miner or the wiper has already run.
The dangerous property here is timing. The window between publish and discovery is where all the damage happens, and that window is measured in hours. Tooling that inspects code after it’s checked out, or scans a lockfile on a schedule, is reading the crime scene — not preventing the crime.
Why “just review your dependencies” doesn’t hold
The standard advice — pin everything, review every upgrade, audit your tree — breaks on contact with reality. A mid-sized project has thousands of transitive dependencies. Nobody reads the diff of a patch bump to a package six levels deep, and the whole point of a patch release is that you’re supposed to be able to take it without ceremony. The trust model of open-source registries is that a version, once published under a name you trust, is safe to resolve. Compromise the maintainer and that assumption is simply false — for every consumer, simultaneously, at install time.
Where the chain breaks
The one link where you can intervene before the artifact is ever fetched is resolution. A package manager doesn’t download a version until it has resolved a range to a concrete version and asked the registry for it. If something sits in that path — not scanning after the fact, but serving the request — it can act in the only window that matters.
That’s what a registry-replacement firewall is. Every install in CI resolves through
it. When a package manager asks for the poisoned 4.17.22, the firewall sees the
request the moment it’s made:
services/checkoutresolveslodash@^4.17.0→ the bad4.17.22is stripped from the available set, the range resolves to the last safe version, the build stays green, and the malicious artifact is never downloaded.- A pipeline pinned exactly to the bad version fails loudly — with the named safe version to move to, not a silent green that hides the exposure.
The malware doesn’t run because it never lands. Not quarantined after download — never fetched. And because the firewall verifies each CI run’s keyless OIDC identity, the record of which pipeline reached for the bad version, on which commit, survives as an audit trail even for the requests it blocked.
We’ll go deeper in later posts — specific payload analysis, the signals that flag a version as bad in the first place, and the honest limits of resolution-time defense (it’s only as good as knowing a version is compromised). But the anatomy is worth internalizing on its own: the attack is boring, the distribution is automatic, and the only comfortable place to stand is in front of the fetch, not behind it.
See how resolution-time blocking works in the dashboard docs, or read the companion piece on why silent rollback beats hard blocking.