Last week, CERT alerted users to the risk of publishing or consuming a malicious npm package. While not unique to npm, this substantial risk is more likely to happen in this ecosystem. It should be noted that while this is definitely an attack vector, in my opinion it’s not a vulnerability as much as an insecure default.
This post explains the risk and how you can protect yourself. We’ll see the tradeoffs that triggered the problem, review an exploit scenario, and share how to mitigate the risk in your own environments. If you just want the action items, jump to the mitigation sections.
Malicious Packages in 5 easy steps
At the heart of the problem lies the ease of publishing a new package. Like git, pip and others, npm allows users to store their credentials in a dotfile (
.npmrc), and then publish without interactive prompts.
Unfortunately, this is one case where you can make something too easy. The streamlined publish process increases the chance of publishing by mistake, but more importantly it opens the door for attackers to intentionally publish malicious code.
To publish a malicious package version, all the attacker needs to do is:
- Run code on the developers machine.
- Determine who’s the logged in npm user (
npm whoami --silent)
- Download one of the user’s packages to a temp folder
- Edit package.json: add a malicious
postinstall hook and increment version by 0.0.1
- Publish (
Of these, the only challenging step is the first one. Unfortunately, most developers tend to run their environments pretty loosely…
Did you ever install something via
curl X | bash? That’s one way for an attacker to get in - they don’t even need
sudo bash. Alternatively, perhaps you occasionally install an npm package locally, just to try it out? Any one if its dependencies can have a
postinstall hook running the above.
Fact is, we developers experiment, which naturally means we’re not using pristine environments. Such environment should not have publishing rights.
Fast propagation, slow detection
Once done, the attacker can just sit back and relax while his code propagates across the Node ecosystem. Most consumers of this package will load it using a semver range that implicitly takes patch (0.0.X) versions, and will take this new version without. They can also make the malicious code propagate itself to packages owned by its victims, creating an even faster propagating worm.
To make things worse, if such a malicious package was published, there’s no easy way to spot it happened. npm doesn’t notify the user when a new release is published, and there’s no simple way to see delta between packages (unlike git). Your best chance at spotting such an issue is noticing the new version number. If you’re actively maintaining a single-developer project, you’re indeed likely to notice version changes, but for dormant projects or ones developed by a full team, those can easily slip through.
How to prevent malicious publishing (public)
If you’re a collaborator in an npm package, it’s your responsibility to protect yourself. Don’t let attackers to post a bad release on your behalf. Doing so is simple: Stay logged out.
Stick to publishing through your CI when a build on a the master branch succeeds, using semantic-release or custom scripts. Here are the step-by-step instructions:
- Invalidate all current tokens. You can do so through your token page, and should do the same for all collaborators.
- Configure a new token on your CI. Remy wrote detailed instructions for Travis, but most CIs support hidden variables. Note that, once your token is configured, running
npm logout will invalidate it. Delete your
~/.npmrc file instead to return to a logged out state.
- For dev versions, use git. If users need access to a temp package version (e.g. when troubleshooting), commit it to a branch and guide them to install via a git commit.
How to prevent malicious publishing (private)
If you’re using a private package, being logged out isn’t an option, as you’ll need to be logged in to have access to your private packages. Fortunately, npm has recently released organizations, and support read-only users, letting you provide access without risking publishing.
Once more, here are the step-by-step instructions for doing so:
- Create an npm organization. If you only have one user, this will bump the minimum cost by $7/month, but is well worth it.
- Create a team with read-only access to your packages or scope.
- Add users to that team as members. You can also reuse one read-only user.
- Have your team (and yourself) login as these read-only users
These steps do not alleviate the need to be careful when installing random software on your computer, but at least they reduce the likelihood of you being a distribution vehicle for malicious code.
How to protect yourself from malicious packages
As a consumer of npm packages, you can’t truly avoid this risk (note this is true for other package managers as well). The only way to mitigate the risk is by controlling which packages you install.
If you’re developing locally, consider using npm shrinkwrap to contain the package versions you use. shrinkwrap freezes the versions you use, not taking new versions unless explicitly asked, and so gives you the opportunity to and manually scrutinize every update. This is cumbersome, but will make you safer.
If you’re an organization, consider using npm On-Site and whitelisting the packages you allow. Again, this will be a bit of a hassle, but will reduce the risk of malicious packages making their way in. To be truly safe here, make sure you set Read through cache to Off.
Both of these solutions will slow down your intake of new versions, and so may leave you exposed when vulnerabilities are disclosed on older versions of these packages. If you choose them, I’d encourage you to stay on top of known vulnerabilities in your npm dependencies, for instance by using Snyk.
Can’t npm just fix this?
Wouldn’t it be awesome if npm can simply throw in a feature or two and make this entire risk go away?
Unfortunately, they can’t. This risk is a natural byproduct of using open source packages. It means we use many small pieces of code written by many people. While awesome for productivity, such crowdsourcing means we are substantially dependent on the authors of those packages and their intents.
It’s also important to note these behaviors aren’t unique to npm. Most other package managers allow silent publish, and practically all of them allow custom package code to run on install. The only reason this risk is greater with npm is the larger number of indirect dependencies used, which make it all but impossible to track them all manually.
That said, there are a few things I would love to see npm team do to help the following:
- Send an email to all collaborators when a new package version is released. This should be the default behavior, optionally allowing users to opt-out later.
npm publish require credentials, unless used with an explicitly generated automation token. This will make the default behavior (using
npm login) safer. This is likely a breaking change, and so I wouldn’t expect it to happen before npm 4.
- If possible, contain the actions installations scripts can perform during installation. This won’t eliminate this risk, but it’ll make it that much harder, and so that much less likely to occur.