npm Has Become a Russian Roulette

npmjs.org is arguably the world’s largest package repository. In 2022 it was estimated to serve over 43 billion downloads every week. I found no recent estimates, but the number should be much higher today.

In the past several weeks, there have been 3 identified large-scale phishing-malware attacks on the npmjs.org:

Playing Russian Roulette

The common theme of these recent attacks was that malware was intentionally published on npmjs.org by attackers and then machines downloading latest versions of popular dependencies became infected.

The Shai-Hulud is notable in that affected packages account for roughly 2 billion downloads a week. This is significant chunk of the whole npm downloads (remember that estimate for total weekly downloads was at 43 billion 3 years ago).

With growing frequency of attacks like those listed above, using npm increasingly feels like a game of Russian Roulette: download the latest version of a popular package at the wrong time, and your system could be compromised.

Russian Roulette game

npm Design Often Misunderstood

Now, the problem with npm is that it is designed to download latest packages under various conditions. At the same time, the mechanics of the download process are not well understood. Particularly, npm client uses at least 4 commands to install dependencies:

  1. npm ci – follows package-lock.json exactly
  2. npm audit fix – tries to update to the latest version of every dependency to fix known vulnerabilities, within the given version constraints
  3. npm audit fix –force – same as before, but ignores version constraints
  4. npm install – follows package-lock.json if it’s present or specific entry is present in it and if not – it installs the latest dependency within the given version constraints and extends the lock file accordingly

Of these 4, npm install is the most used (nearly every developer README for a node project starts with “do npm install), and at the same time the most confusing and least understood in terms of what it’s actually doing.

yarn and pnpm also share similar behaviour with small variations.

It is also important to note 2 things here:

  1. The tooling essentially follows not only the project’s package.json but also those of its dependencies and sub-dependencies. It’s practically impossible to control versioning constraints in those transitive and predict how the versions would be resolved.
  2. By default, npm is using post-install scripts that are run automatically on npm install and other install commands mentioned above. Such scripts could be a source of hidden malicious behaviour, they can be disabled with --ignore scripts flag or globally using npm set ignore-scripts true – but this may break some dependencies.

No Silver Bullet

As of this writing, there is no silver bullet to fully protect an organization from npm attacks like the ones mentioned above, but there are things that reduce the chance of getting malware significantly. I will put them in order of importance:

  1. Only use npm ci in your CI pipelines. This will guarantee that you are only pulling known dependencies from your existing package-lock.json in the CI context and will protect your live environments. Also, locally, whenever you are not planning to change dependencies, use only npm ci.
  2. Quarantine period – internally, at Reliza, we have added a quarantine filter on top of Verdaccio project and spun our npm mirror with it to only allow packages older than 10 days (configurable). This matters because in all attacks listed above malicious package were removed from npmjs.org quickly – typically, in less than 24 hours. Therefore, quarantine period is actually the most important thing to boost your defences. We also published a corresponding helm chart here – note that this one is a little bit custom to our environments (we’re using Traefik heavily).
  3. Disable post-install scripts locally via global npm setting (npm set ignore-scripts true) and across your pipelines (via --ignore-scripts flag) if possible.
  4. Use tools like our ReARM or plain Dependency-Track to be able to track if you ever had malicious package in your known supply chain. With such tools you would also be notified of an issue, but note that there is a delay involved.

Bad Advice

Unfortunately, since this whole npm saga is top of the mind right now, I also saw some bad advice on the Internet of how to mitigate the problem.

Notably, there was at least one article suggesting that you should remove version constraints from package.json, e.g. changing version from something like “^1.0.0” to something pinned like “1.0.3”.

All this does is creates a false sense of security where you’re missing out on subsequent vulnerability patches.

Why is this a false sense of security? Remember transitive dependencies with their own package.json files – you have no control over those, and it’s usually these transitive dependencies that would pull some of malicious packages.

Also, there is now proliferation of tools that do string / regex matching on malicious packages and block them on install. While having those is not a bad thing, it is important to understand that:

  1. Attackers may change package identity at any time while the attack is in progress
  2. These tools are always reactive – so in the absence of quarantine period you’re still playing the same Russian Roulette, but maybe at a slightly better odds (depending on when the safety tool will receive an update about the new malicious packages).

TL&DR

npmjs.org security is a huge problem right now. While not a silver bullet, I recommend following 4 things to boost defences:

  1. Use only npm ci in CI environments and locally when not planning to update dependencies.
  2. Quarantine period (can be achieved using our filter on top of Verdaccio)
  3. Disable post-install scripts locally via global npm setting (npm set ignore-scripts true) and across your pipelines (via --ignore-scripts flag) if possible.
  4. Use tools like ReARM or Dependency-Track to track if you were affected by malware at any point.

Leave a comment

Your email address will not be published. Required fields are marked *