Tips for Using a Package Manager

Posted On: 2025-10-06

By Mark

In the past 30 days there were (at least) 3 different supply-chain attacks against the Node Package Manager (npm), the most severe of which was a self-replicating worm (dubbed "Shai-Hulud") that used its victims' credentials to push malicious package updates to the npm package repository. In today's post, I'll cover some advice for using package managers safely - with a focus on protecting against the way that Shai-Hulud propagated.

What is a Package Manager?

At the risk of being overly simplistic, a package manager is a tool to make installing software easy. Package managers are everywhere - from Node's npm, to C#'s NuGet, to Debian's APT, to (arguably) the iOS App Store. The exact specifics and scope of their tasks varies, but npm (and other developer-focused package managers) help developers to make sure not only that the developer is getting the package that they expect, but also make sure that package has all the dependencies that it needs*.

* As well as those dependencies' dependencies, and so on. It is notoriously painful to manage dependencies by hand, and package managers are designed to eliminate as much of that pain as possible.

Version Ranges and Pinning

Like most package managers, npm allow developers to specify not just the package, but also the version that they intend to install. Since many packages are regularly updated, developers can opt to specify a range of acceptable versions - such as "version 2 or later" or "the latest security patch for 3.0". Developers can then easily invoke npm update to get the latest version of all their dependencies matching the versions specified.

Version pinning, on the other hand, is when the developer specifies a specific version (rather than a range.) In those cases, npm update will always get that exact version, and nothing else. Paradoxically, version pinning is considered a bad security practice in some situations and a good practice in others - pinning means you won't get security fixes unless you manually change the version number, but it also means you won't automatically install a malicious update that is masquerading as a security fix (which is how Shai-Hulud spread.)

The Dangers of Install Scripts

While version pinning provides a solution to known malicious packages, it does nothing to help discover them. Instead, developers (generally) have to rely on auditing any packages they install - making sure that each does what is intended and nothing else. The simplest approach is installing the package locally and directly inspect its contents, but, as demonstrated by Shai-Hulud, npm allows package authors to run code at install time - before a developer even has a chance to audit its contents.

For npm specifically (not necessarily other package managers), there are a few ways to work around this. Firstly, and most simply, the npm registry has a web interface that developers can use to audit a package prior to installing/upgrading. Secondly, npm provides an optional flag ignore-scripts which can be set via the command line, npmrc file, or any other method. As described in the documentation, any commands run with that flag active will ignore all scripts in the package - making it possible to install/upgrade packages with a bit more safety (although activating that setting globally may impair some packages' functionality*.)

Be Mindful of Package Locks

One defense mechanism that npm provides is a package lock: this file functions as a kind of precise version pin, documenting the exact version, source location, and hash for every installed package. It can safeguard against a whole host of issues - both security-related and general functionality-related.

Unfortunately, npm is quick to modify or outright override the package lock if the project isn't set up exactly right. This is, in large part, due to the package.json being the resource used to generate the package lock: in the event of a discrepancy, the package.json will (usually) win. While there are some commands (such as npm clean-install) that use the package lock exclusively, npm generally seems to silently ignore/resolve discrepancies between the package file and the package lock file, increasing the risk of hidden version differences. If you choose to rely on package locks, it's best to be vigilant about it: make sure the packages in the lock file are all within the ranges defined in the packages file, and make sure every future change to the packages file has an accompanying lock file change.

Conclusion

In researching this post, I learned quite a bit about npm, and it's my hope that what I've written will be equally useful for you. At the risk of sounding pessimistic, I fully expect that we'll see more supply chain attacks in the future - but hopefully what I've explained here will be useful against any copycat attacks. Regardless, I hope you enjoyed reading, and if you have any thoughts or feedback, please let me know.