On September 8, 2025, an npm maintainer clicked a phishing email from a lookalike domain, npmjs.help, promising a two-factor reset — and within hours, malicious versions of 18 packages including debug and chalk, with a combined 2 billion weekly downloads, were live on the public registry. The payload hooked fetch and XMLHttpRequest to silently reroute cryptocurrency transactions through wallet APIs like window.ethereum. The compromised versions stayed up for roughly two hours before removal, but researchers at Wiz and Aikido estimated they reached close to 1 in 10 cloud environments before that window closed. Two months later, on September 16, 2025, a second and more automated incident emerged: the Shai-Hulud worm, which harvested npm publishing tokens and GitHub credentials from developer and CI machines to self-replicate across more than 500 packages, prompting a CISA alert on September 23. A follow-on wave in late November 2025 hit roughly 25,000 malicious repositories tied to around 350 accounts. Neither incident involved a typosquat — both compromised packages your build already trusted. That distinction is the whole argument for this post: name-based allowlisting doesn't help when the name was never the problem. Pinning, lockfile verification, and controlled registry access do.
Why didn't typosquat detection catch either 2025 incident?
Typosquat detection catches names — reqeusts instead of requests — not accounts. Both the September 2025 npm attack and the Shai-Hulud worm compromised packages that were already legitimate, widely-used, and correctly spelled: debug, chalk, ansi-styles, and hundreds of others pulled into the worm's self-replication chain. The attacker in the npm case didn't publish a new package; they gained publish rights to an existing one via a phished 2FA reset and pushed a malicious version under the real name, to the real registry, with the real maintainer's credentials. A scanner built to flag suspicious new package names or unfamiliar publishers has nothing to flag — the publisher is exactly who it's supposed to be. This is why supply-chain hardening has to shift from "is this package what it claims to be" to "should this build be allowed to silently accept whatever the registry serves it right now," which is a build-configuration question, not a naming-pattern one.
What does registry pinning actually stop?
Pinning stops your build from automatically accepting a new version the moment it's published — which is the exact mechanism both 2025 incidents relied on. A package.json dependency declared as ^4.3.4 lets npm install silently resolve to any compatible version published after your last install, including one published two hours ago by a phished maintainer. Pinning to an exact version (4.3.4, no range operator) or, stronger still, to a resolved content digest, means a CI job only ever installs the version a human or a review process explicitly approved. This wouldn't have prevented the initial compromise of the debug package itself, but it would have kept every downstream build that already had a pinned, known-good version from silently drifting onto the malicious one on the next npm install — which is precisely the mechanism that let Shai-Hulud self-replicate through automated CI installs using loose version ranges.
How do lockfile integrity checks add a second layer?
A lockfile (package-lock.json, poetry.lock, Cargo.lock) records the exact resolved version and integrity hash of every dependency, including transitive ones, at the time it was generated. Running npm ci instead of npm install in CI enforces that the lockfile is honored exactly — the build fails rather than silently re-resolving if the lockfile and manifest disagree, or if a registry serves content that doesn't match the recorded hash. This matters because pinning a top-level version doesn't protect you from a compromised transitive dependency several layers down that your manifest never references directly; debug and chalk are exactly this kind of ambient transitive dependency, pulled in by hundreds of other packages without ever appearing in a project's own package.json. A lockfile with hash verification, checked into source control and diffed on every PR, turns "a transitive package changed underneath us" from invisible into a reviewable line in a pull request.
Why add a private mirror on top of pinning and lockfiles?
Pinning and lockfiles stop your existing builds from drifting onto a bad version, but someone still has to approve the first pull of any new or updated package — and a private mirror or proxy is where that approval gate lives. Instead of every CI runner and developer machine fetching directly from registry.npmjs.org or PyPI, requests go through an internal proxy (Verdaccio, Artifactory, Nexus, or a purpose-built install-time firewall) that can hold new versions for review, scan them before release, or block known-bad publishers outright. Because the proxy sits on the actual fetch path, it sees transitive dependencies too — the same ones a manifest-only allowlist would miss. This is also the layer that limits blast radius across an organization: if one team's proxy configuration flags a package during the two-hour window a compromise is live, other teams pulling through the same mirror never see the malicious version at all, rather than each repository independently rolling the dice against the public registry.
How Safeguard Helps
Safeguard's Package Firewall implements the mirror-and-gate pattern directly: it runs as an install-time proxy in front of npm and pip (or fronting an existing Artifactory or Nexus instance), so every fetch — including transitive ones — is evaluated before a package reaches a build, with allow, warn, block, and quarantine modes and a 14-day auto-release window for packages awaiting a cleared verdict. For teams that want to skip registry roulette entirely, Safeguard's Gold registry offers a curated, drop-in mirror of over 6,000 artifacts across npm, PyPI, Maven, and other ecosystems — each continuously revalidated for zero critical or high CVEs and zero malware, signed with a Sigstore identity, and re-scanned automatically on every vulnerability feed update, so a newly compromised upstream version never propagates into a Gold-sourced build in the first place.