Avoiding npm substitution attacks

Image of Isaac Z. Schlueter

Supply chain attacks are a reality in modern software development. Thankfully, you can reduce the attack surface by taking precautions and being thoughtful about how you manage your dependencies. We hope you walk away from this with tangible steps to take to ensure you’re protecting yourself when using npm.

This post is focused on npm, but for further reading of prevention measures against supply chain attacks for other package managers, check out this whitepaper from Microsoft.

TL;DR

  1. Use scopes for internal packages.
  2. Use a .npmrc file in the root of a project to set the intended registry.
  3. Take care when proxying.
  4. Respond quickly to build failures.

Scopes

In npm, a “scope” is a @-prefixed name that goes at the start of a package name. For example, @my-company/foo is a “scoped” package. You use scoped packages just like any other module name in package.json and your JavaScript code.

{
  "name": "@mycompany/foo",
  "version": "1.2.3",
  "description": "just a scoped package name example",
  "dependencies": {
    "@mycompany/bar": "2.x"
  }
}
// es modules style
import foo from '@mycompany/foo'

// commonjs style
const foo = require('@mycompany/foo')

Scopes were introduced in 2014 and are supported by all popular npm clients today.

Why and how to use scopes

Scoped packages on the public npm registry may only be published by the user or organization associated with it, and packages within that scope may be made private. Also, scope names can be linked to a given registry.

For example, this login command will ensure that all requests for packages under the @mycompany scope will be made to the https://registry.mycompany.local registry. Any other requests that are not bound to a scope will go to the default registry.

$ npm login --scope=mycompany --registry=https://registry.mycompany.local

This saves the login information in your ~/.npmrc file as follows:

@mycompany:registry = https://registry.mycompany.local/
//registry.mycompany.local:_authToken = xyzabc123-arbitrary-token-value

Once that is done, head over to the public npm registry, and create a free organization with the “mycompany” name. At that point, no one else can publish anything under the @mycompany scope on the public registry, and your builds will fail with 404 errors if they’re misconfigured, rather than silently fetching untrusted content. This is an important step, because it prevents an attacker from hijacking your organization’s name in the public registry, which could result in the exact same problems.

As a final precaution, you can create a .npmrc file in the root of your projects, with a line like this:

@mycompany:registry = https://registry.mycompany.local/

By doing this, npm will associate the scope with your internal registry when working with that project.

Bonus: Avoiding non-malicious failures

Using scopes prevents additional failures which, while not intentionally harmful, can be annoying and hard to debug. For example, say you are using an internal package named foo. Later, a user on the public registry chooses the name foo for a public package. Someone else publishes a public package bar that depends on foo (the public version, not your internal foo).

One day, you try to use bar (the public package) in your internal environment. It tells npm that it needs a dependency foo. npm dutifully fetches foo, and since it is configured to point to your internal registry, it finds the internal package foo. Unfortunately, this is a different thing entirely than the public package foo, and your build breaks when you least expect it.

Scopes also prevent leakage that occurs if someone accidentally publishes private packages to the public npm registry. If no one is in the public org, then even if they mess up the configuration somehow, the publish will fail, because they do not have permission to publish to that scope.

These failures aren’t necessarily malicious, but they are annoying. No one likes a fire drill. Use scopes, and avoid the hassle.

Scopes close this attack vector (not all attack vectors)

Software security is never a black and white issue, but scopes can mitigate the risks posed by the specific attack vector described, where an attacker claims a name on the public registry that you are using internally.

If you are using a private npm registry implementation that does not support scopes, please talk with your vendor and let them know that you need this feature for your supply chain security.

If you haven’t been using scopes, it could be a big undertaking to migrate your package names. In the meantime, while you are doing this important migration for security, there are some other things you can do.

Proxying

It’s a common practice to use an internal registry that is configured to proxy any packages from the public registry that it doesn’t already know about. You publish your private packages into this registry, so it “shadows” the public registry.

If you are using scopes, this is probably fine, as shown above. If you are not, then additional care must be taken.

Do not proxy internal package names

First, make sure that your internal registry does not proxy any package name that has already been published into it. In the attack in question, a malicious user may publish a package to the public registry with the same name as one of your internal packages. If your registry does not proxy anything by that name, then you’re protected as long as you are only making requests to your internal registry.

Especially, ensure that your private registry is not configured to “merge” manifests of the same name from the upstream public registry. This is sometimes enabled to work around resolution collisions, but it is a very bad idea, precisely because “work around resolution collisions” is how name hijacking exploits work. If possible, use a private registry implementation that doesn’t even have this feature.

You’ll still be vulnerable to the non-malicious resolution collisions, which can be costly and annoying to deal with, but you won’t be compromised as easily.

Configure projects to use internal registry

Second, make sure that all internal projects have a .npmrc file in the project root, setting the registry value to your internal registry.

registry = https://registry.my-company.local/

This will avoid issues where a developer or build environment checks out the repo and runs npm install, unwittingly fetching untrusted content, as well as preventing anyone from publishing the package to the public registry by accident.

You can check the currently configured registry at any time by running this command:
$ npm config get registry

Internal packages should be immutable

Once a package is published to the internal proxy registry, it is very important that you do not silently fall back to the public registry if that package is ever removed.

For example, you might publish mycompany-foo to your internal registry. Some time later, you decide to no longer use this package and remove it from the internal registry. If the proxy then starts serving this package name from the public registry, you are in a situation where an attacker can take over the name and gain access to any systems that are left behind.

This is why the public npm registry has strict rules about what can be removed, and even when a package is removed, its name and version number can never be reused.

Treat proxied data as untrusted

The npm CLI does not have any special treatment for the public npm registry, apart from being a default config value. If you override that config setting, then npm will use the value you provide, and you will have reduced one vector for outside content to get in.

However, an internal proxy should be considered exactly as trusted as the data it proxies. If you have it configured to merge upstream manifests with your local private packages, or allow internal package names to be proxied, then you are vulnerable to an attack.

Never ignore build failures

If you configure your projects as we suggest, you will tend to see a 404 error rather than fetching untrusted content.

Do not ignore these errors! Configure your systems to crash as loudly as possible if a build fails, and fix it right away when this happens. Supply chain attacks rely on doors staying open. If you ignore the alarms, you’re neglecting a powerful tool for addressing problems.

Supply chain security is within reach

There are many tools at your disposal to help keep your software supply chain as safe as possible. The most important thing is to make sure that you use the tools available, and understand the trade-offs when making decisions.
Now that you know the basics, it may be a good idea to check out GitHub Packages, which helps you adopt most of these best practices by default.

The npm command line interface roadmap also includes more features to make it even easier to keep your builds secure and reliable. If you want to get more involved, we always welcome community engagement in our process.