TLDR
A malicious commit in Zod, the most-installed TypeScript validation library, shipped a loader whose command-and-control addresses live in an Ethereum mainnet contract that cannot be taken down, plus a rewired release workflow so any version-bump commit to main would publish to npm by itself. It enables whoever held a compromised GitHub credential to turn 30-million-weekly-install infrastructure into remote-code-execution with one commit. The difference between this attempt and a successful attack is that the npm tarball of 4.6.6 shipped clean, and the tell for the whole class of attack is that the payload’s location was chosen for persistence, not stealth.
Caption: the five hops from a compromised credential to a payload; the middle hop is immutable blockchain state, which is why this attempt’s architecture matters beyond this incident.
The loader, in four lines
The added file masquerades as a test. It imports three modules, sets three constants (a contract address, a four-byte ABI selector, and three RPC URLs), then does exactly one thing: an eth_call against mainnet asking the contract for a value, decoded into an array of strings. Each string is a hostname. The loader fetches /loader on each until one answers with JSON containing three fields, writes the base64-decoded payload to a temp directory with its declared name, and imports a base64 data-URL to run the setup blob. The design’s two virtues, from the attacker’s side: the stage-1 on disk never contains a linkable indicator beyond public RPC endpoints, and the C2 rotation is a state write on the contract, which costs the attacker gas and costs a defender nothing to lose.
Caption: the chain: commit to loader, loader to mainnet, mainnet answers with hosts, hosts answer with a payload. Every hop except the payload fetch is ordinary developer-grade infrastructure.
The npm question, answered from the tarball
The thread’s first hour converged on “nothing reached npm.” The registry says otherwise and also says it shipped clean. Zod 4.6.6 was published October 11 at 16:18:23 UTC and stands as dist-tags.latest; its actual tarball contains neither meta.test.mjs nor any preinstall hook. So the 4.6.6 release that reached the registry after the malicious commit was built from a publish path the attacker’s injected files never entered, and the repo’s HEAD (last push 16:19, one minute after the publish) no longer carries the loader file. The two readings are different and both matter: either cleanup happened between the commit landing in main and the release firing, or the malware’s delivery mechanism failed on its own.
The misplaced “preinstall” key, sitting at the package.json root instead of inside “scripts”, would have done nothing on current npm because npm does not read lifecycle hooks from the root object. The attacker’s commit expected its own key to run, which points to a hasty or generated diff rather than a rehearsed operator. The harder finding is that the loader only matters if a release publishes from the poisoned tree, and the same commit removed exactly the friction that made the release safe: the workflow comment that a version bump on main publishes nothing without a maintainer manually running and approving the npm-environment job was deleted, replaced by a push trigger watching the two packages’ package.json files. The change reads as an attempt to remove the human from the loop permanently, and the automation rewiring looks like the commit’s real payload.
What the HN thread got wrong and what it got right
The thread corrected itself fast. The early comments scored the finding as near-certain compromise, one noting “this is so bad it doesn’t even try to hide itself.” The correction that matters came from the misplaced preinstall key: the injected trigger would not have executed, “so from what I can tell this wouldn’t run on install,” followed by the reminder that npm advisories can still flag it and that disabling postinstall-class scripts by default is the cheap defense. The final read, “a GitHub credential just got compromised, happens all the time,” is the accurate one for the incident class; the Ethereum C2 is the novel part, the credential compromise is the boring, recurring part.
What to watch
- The GitHub security advisory and whether npm deprecates 4.6.6 anyway; a version that is technically clean but adjacent to a compromised credential belongs on the advisories radar for rotation guidance.
- Whether the maintainer publishes a postmortem naming the credential path (GitHub PAT phishing, token leak in CI logs, or a compromised maintainer session), since the September 21 commit date means access existed for three weeks.
- The Ethereum contract’s future use: the address is public, and monitoring that selector on mainnet now yields a live feed of the attacker’s infrastructure rotation, if anyone is watching.
- The release.yml change surviving or being reverted: the push-triggered publish, not the malware file, is the durable risk if it remains.
- Whether Zod’s CI gains the environment-approval gate back: the npm environment approval existed and the commit removed the need to pass it.
Sources: HN thread - the malicious commit - npm registry: zod 4.6.6
Related on this site: Serverless alarm loop: the $10,811 Cloudflare bill
Discussion
Be the first to commentStart a discussion
Got a take on this, a rig to show off, or a benchmark that says otherwise? Sign up and start the thread - your comment publishes instantly once you're in.