Compromised npm packages: what happened and how a cooldown period protects you - init.d
IT

# Compromised npm packages: what happened and how a cooldown period protects you

Alessandro Corbelli~7 min
Table of Contents

JavaScript developers install third-party libraries dozens of times a day, almost without thinking. One command, a few seconds, and the project has a new dependency - along with all of that dependency’s dependencies. Over the past months this everyday gesture has become the delivery channel for a series of attacks that hit libraries used by millions of projects. The good news: one of the most effective defences is also easy to adopt - waiting a few days before installing freshly published versions. Let’s look at what happened and how to configure this protection with pnpm.

What a supply chain attack is

Terms first. The npm registry is the public archive JavaScript packages are downloaded from: it holds millions of libraries, published by their maintainers. A supply chain attack doesn’t target your project directly: it targets one of the libraries your project uses. If an attacker manages to publish a modified version of a popular library, everyone who installs it during that window runs the attacker’s code too.

The weak point is almost always the maintainer’s account. Once the attacker obtains their credentials - usually through phishing, i.e. counterfeit emails and websites imitating the real ones - they publish a new version of the package with malicious code inside. To anyone installing it, that version is indistinguishable from a legitimate update.

What happened, briefly

The recent episodes give a sense of the scale.

In September 2025 a very active maintainer was fooled by a phishing email imitating npm support (the counterfeit domain was npmjs.help). His credentials were used to publish malicious versions of 18 packages, including chalk and debug: tiny libraries that sit, directly or indirectly, inside a huge share of the ecosystem - together they count billions of downloads per week. The injected code hijacked cryptocurrency transactions in the browser. The compromised versions stayed online for about two hours before being removed.

A few weeks later came Shai-Hulud, a new kind of attack: a worm, a program that replicates on its own. Starting from the @ctrl/tinycolor package, once installed it searched the victim’s machine for credentials and access tokens (npm, GitHub, cloud providers), used them to publish infected versions of the victim’s own packages, and so spread from maintainer to maintainer. The affected packages reached into the hundreds, and the stolen secrets were published in automatically created GitHub repositories. In late November 2025 a second wave (“Shai-Hulud 2.0”) produced tens of thousands of malicious repositories.

The phenomenon has not stopped: in the spring of 2026 researchers documented new campaigns reusing the same techniques - including a third wave of the worm starting in late April - and in June 2026 about thirty packages published under the @redhat-cloud-services namespace were compromised. This is not a complete list, and it will most likely not be the last chapter.

Why “waiting” is a defence

One detail recurs in nearly all these episodes: malicious versions are detected and removed fast. The compromised versions of chalk and debug lasted a couple of hours; the Shai-Hulud worm was identified within a dozen hours. The security community monitors the npm registry constantly, and a popular package that starts doing strange things gets reported almost immediately.

The problem is that during those few hours the damage hits whoever installs right at that moment. And the version ranges used in package.json (the classic ^1.2.3, which accepts any newer compatible version) mean a freshly published version is adopted automatically by anyone installing from scratch or updating.

Hence the idea of a cooldown (or quarantine): never install versions published less than X days ago. It doesn’t protect against everything - an attack that goes unnoticed for longer will get through - but it would have blocked nearly every episode seen so far, at the cost of adopting updates a few days late.

Configuring it with pnpm

pnpm (a package manager alternative to npm, compatible with the same registry, which can also pin the Node.js version a project uses) implements this idea with the minimumReleaseAge setting, expressed in minutes. It goes in the pnpm-workspace.yaml file at the root of the project:

pnpm-workspace.yaml
# don't install versions published less than 3 days ago
minimumReleaseAge: 4320

From then on, for every dependency - including transitive ones, the dependencies of your dependencies - pnpm resolves the most recent version that is at least that old. The setting has existed since pnpm 10.16. An example tested while writing this article: as of July 16, 2026 the latest version of @types/node is 26.1.1, published on July 8. With a 10-day cooldown (minimumReleaseAge: 14400), pnpm skips 26.1.1 and installs 26.1.0 from July 1, saying so openly:

Terminal window
$ pnpm add @types/node
# ...
dependencies:
+ @types/node 26.1.0 (26.1.1 is available)

No error, no manual step: the too-recent version is simply not considered, and pnpm notes in parentheses that it exists. Once it has aged past the threshold, a normal update will bring it into the project.

The check doesn’t apply only to new installs: pnpm also verifies the existing lockfile, and if it contains a version younger than the threshold it blocks the install with an explicit error (ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION - we tested this too). If a specific dependency must be allowed through immediately - an internal company package, or an awaited security fix - you add an exception:

minimumReleaseAge: 4320
minimumReleaseAgeExclude:
- "@our-company/*"

How long should the cooldown be? Since pnpm 11 the setting defaults to 1440 minutes (one day), which is already enough for flash episodes like chalk/debug. A value between 3 and 7 days gives more margin, in exchange for slightly less timely updates: a reasonable choice for projects that don’t chase the latest version of everything.

The other defences pnpm enables on its own

The cooldown works best together with two other protections, already active in recent pnpm versions.

The first concerns install scripts. Many attacks (Shai-Hulud included) trigger through lifecycle scripts, commands a package can run automatically at install time. Since pnpm 10 these scripts are no longer executed for dependencies, except those explicitly allowed (the current key is allowBuilds; earlier versions called it onlyBuiltDependencies):

pnpm-workspace.yaml
# only these packages may run install scripts
allowBuilds:
- esbuild

The second is the lockfile (pnpm-lock.yaml), the file recording the exact versions installed. In CI and production it’s worth installing with pnpm install --frozen-lockfile: if the lockfile doesn’t match package.json the install fails instead of resolving new versions, so no update gets in without going through a reviewable change, the same spirit behind scoping the tokens your pipeline uses down to the minimum they need instead of leaving them broad.

npm users aren’t left without options: the --before flag installs only versions published before a given date (npm install --before="2026-07-09"), though it must be passed on every command; as of today there is no stable equivalent of pnpm’s automatic cooldown.

In summary

npm supply chain attacks are no longer isolated episodes: between late 2025 and 2026 they hit everyday libraries, with a worm able to spread between maintainers on its own. Almost all malicious versions, however, are removed within hours or days of publication. Quarantining versions that are too recent - with pnpm’s minimumReleaseAge, a value between 1 and 7 days - removes the window where you are most exposed, without changing how you work. Together with blocked install scripts and a frozen lockfile in the pipeline that builds the project, it is one of the best cost/benefit defences available to JavaScript developers today.

Sources

Tux versione Gandalf, mascotte del blog init.d

init.d is the team led by Alessandro Corbelli, a Linux systems administrator and backend developer with over twenty years of experience. He designs and runs cloud infrastructure (Google Cloud, AWS, Azure), server farms and high-availability architectures, and builds custom software in Laravel/PHP and Vue - from the Take2Me food delivery platform to our clients’ management tools. On this blog we share technical notes on Linux, system administration, development, DevOps and e-commerce.


More Posts