Build1 publisher3 min readPublished
npm ci verifies downloads against the SHA-512 integrity hash stored in the lockfile
A dev.to writeup credits OpenAI's crawler traffic with surfacing a cache-key collision in RubyGems' CDN that served gemspecs under the wrong package names. For npm shops, the lockfile hash covers the tarball; the lookup that chose it is a separate problem.
The Engineer · Build desk

What happened
- A caching bug in RubyGems' production infrastructure went unnoticed until OpenAI's crawler bots, indexing package metadata at high frequency, generated enough unusual traffic for someone to spot it.
- Tenderlove's writeup, as cited in the post, reports finding mismatched gemspecs being served under the wrong package names.
- Nobody found signs of malicious exploitation before the bug was discovered.
- The post's npm remedy is npm ci, which refuses to modify the lockfile and validates every downloaded package against the SHA-512 recorded for it.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- constraint The integrity hash only exists for versions already pinned, so the install that first adds a dependency has nothing to compare its download against.
- decision A CI job still calling npm install is taking resolution drift over a hard failure, and changing that needs a committed lockfile that already matches package.json.
- exposure Consumers cannot audit the CDN in front of registry.npmjs.org, so the client-side hash check is the only part of the delivery path they control.
- capability Teams on npm 9.5 or newer can verify provenance signatures against the registry today, at the cost of running a second command outside the install.
Follow the bytes on an npm install. The client fetches a metadata document for each package, picks a version that satisfies the range, then fetches the tarball for that version. The integrity field in package-lock.json is a SHA-512 of the tarball npm expects to install [11]. That value only exists for versions already written into the lockfile, and it was recorded from whatever the registry served the first time the dependency resolved.
Which layer got poisoned decides whether the hash helps. The dev.to post describes a cache-key collision that could return one package's metadata, and in some edge cases gem contents, for a request meant for a different package or version [4]. Substitute a tarball and npm ci throws an integrity error [13]. Substitute the metadata document and the damage lands at resolution, before a hash for that artifact exists: adding a new dependency, or letting plain npm install move a package to a newer in-range version, which the post calls silent resolution drift [14].
For the RubyGems finding to transfer, npm's cache key would have to collide the same way. The post's argument is structural. registry.npmjs.org sits behind Cloudflare, and npm, PyPI and crates.io all run comparable caching layers [10][9]. It says "cache-key logic bugs are a class of vulnerability, not a one-off Ruby mistake" [20]. A transfer takes two things: npm's key normalising away something that distinguishes two requests, and a client reading the poisoned object without a pinned hash to check it against. The post shows neither for npm.
Provenance is the other control on offer. npm audit signatures, available since npm 9.5, verifies package provenance signatures against the registry [15]. It defeats a cache substitution only if the signing happens upstream of the cache and the client checks the signature on the copy it actually installed. The post lists it as a command to run alongside npm audit --audit-level=high [15].
The chain of evidence is one dev.to post, originally published at adityarawas.in, which attributes the discovery of mismatched gemspecs to Tenderlove's writeup [18][6]. The supplied text is undated and cites no CVE or RubyGems advisory [19]. It reports that nothing indicated malicious exploitation before discovery [7], and that an attacker who understood the cache-key logic could have engineered collisions so a popular gem name resolved to attacker-controlled code for some subset of installs [8].
The npm remediation holds even if that account is incomplete. npm ci fails on a hash mismatch whether or not a CDN ever collided a key [13]. The cost is procedural: npm ci refuses to modify the lockfile [13], so the pipeline needs a committed lockfile that already matches package.json, and every dependency bump becomes an explicit local step. In my view that is worth paying in CI. In this threat model, cache poisoning is the one attack class that works without a compromised maintainer account or a published malicious package [16][17].
What to watch
- A RubyGems advisory or a dated writeup with the cache-key details. With those in hand, anyone could check whether npm's key space has the same shape.
- Whether npm moves provenance signature verification into the install path instead of leaving it in a separate audit command.
- Any reported npm metadata mismatch, since the lockfile integrity hash does not cover the resolution step.