5

Detect and prevent dependency confusion attacks on npm to maintain supply chain...

 2 years ago
source link: https://snyk.io/blog/detect-prevent-dependency-confusion-attacks-npm-supply-chain-security/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Detect and prevent dependency confusion attacks on npm to maintain supply chain security

Liran Tal

September 13, 2021

On February 9, 2021, Alex Birsan disclosed his aptly named security research, dependency confusion. In his disclosure, he describes how a novel supply chain attack that exploits misconfiguration by developers, as well as design flaws of numerous package managers in the open source language-based software ecosystems, allowed him to gain access and exfiltrate data from companies such as Yelp, Tesla, Apple, Microsoft, and others.

This security research sparked attention, and with it, a new breadth of tools to help organizations detect if they are vulnerable or susceptible to potential dependency confusion attacks.

In this article, I will introduce you, step-by-step, to the dependency confusion attack and how it manifests for JavaScript and Node.js developers working in the npm ecosystem. We will also take a look at the a new, open source tool from Snyk that allows you to detect potential dangerous dependency confusion implications in your own source code repositories: snync.

Pop quiz! Can you guess what snync stands for? Answer at the end…

In this article, we will learn about each of the ways this supply chain attack manifests, and detail how to mitigate each of them:

  • Private npm registry misconfiguration
  • Private npm registry fetches latest versions
  • Manual package updates may introduce malicious versions

Do dependency confusion attacks impact you?

The dependency confusion attack only works on organizations that rely on internal source code libraries. Managing private packages due to the need to maintain intellectual property is very common, and as such, many organizations find themselves using internal proxies, caches, or private package hosting registry services to do just that.

If an organization is managing an internal private package, then this package will (by definition) not exist on public registries and their mirrors. And since the private package is not listed on a public registry, anyone else is free to reserve that package name and potentially launch a dependency confusion attack against you. The fact that a private package can have the same name as a public package is what lies at the heart of this attack.

For the JavaScript and Node.js ecosystems, the dependency confusion attack surface is greatly diminished if you are relying on scoped packages as a reserved namespace. But please note that, regardless of whether you are using npm or yarn, you are vulnerable to this supply chain attack.

Replaying dependency confusion attacks

To practically explore supply chain security attacks in the form of dependency confusion, we will experiment with a hands-on tutorial that will demonstrate the vulnerability and how to mitigate against it.

The following is the package manifest for a project known internally as the Death Star. It’s a great name, I know! 

{
  "name": "the-death-star",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "start": "node server.js",
  },
  "license": "ISC",
  "dependencies": {
    "debug": "^4.3.2",
    "death-star-secret-hyper-matter-reactor": "^1.0.0",
    “superlaser”: “^1.0.0”
  }
}

This code is located in the package.json, and it also shows the dependencies of this project. As you can see, superlaser is a dependent package. Of course, this is a very secret weapon that the empire possesses, so it is only published internally. The Death Star needs to be mobile in space, and as such, it also relies on the private package death-star-secret-hyper-matter-reactor.

How to set up private npm registry

If you want to follow along at home, we have to take a quick detour from the primary article focus to set up a private npm registry. With a private registry, you can experiment on your own and replicate this dependency confusion supply chain attack end-to-end.

To do that, we will set up an internal private npm registry and proxy server using Verdaccio, an open source project for those needs. If you have Docker installed, we can spin it up rather easily as follows:

docker run -it --rm --name verdaccio -p 4873:4873 verdaccio/verdaccio\

If everything is successful, Verdaccio will then greet us:

Congratulations. You now have your own dedicated private hosting of npm packages running locally at port 4873.

Let’s go ahead with publishing our secret internal npm packages superlaser and death-star-secret-hyper-matter-reactor. We will start first with adding a user to this new Verdaccio private registry:

npm adduser --registry https://localhost:4873

Next is our superlaser package manifest file package.json:

{
  "name": "superlaser",
  "version": "1.0.0",
  "description": "Our secrets weapon",
  "main": "index.js",
  "scripts": {
    "start": "node index.js",
  },
  "license": "ISC"
}

The death-star-secret-hyper-matter-reactor package manifest is similar, just with a different package name. Let’s go ahead with publishing it to our private npm registry:

npm publish  --registry https://localhost:4873

Why does dependency confusion exist?

There are primarily three cases that could lead to this class of software supply chain attacks:

  • Misconfiguration on a developer or test server
  • Newer versions of packages published in the public npm registry
  • Arguably, design flaws in package managers

Let’s revisit each of these cases.

Private npm registry misconfiguration

When a developer or a continuous integration (CI) system clones the source code of the the-death-star project — which has the internal superlaser dependency — how does it obtain this dependency?

It likely needs to satisfy the following criteria when an npm install command is invoked:

  1. It needs the URL of the private npm registry where this internal package exists.
  2. It needs a token or credentials of some sort to access that private registry.

The very first step outlined above is where things can go wrong. To specify a particular private npm registry, one needs to explicitly provide configuration information for the npm package manager.

Now let’s revisit some scenarios:

  1. What happens if the continuous integration system doesn’t have the private registry set?
  2. What happens if you are a new developer onboarding to an existing project and you did not undergo prior steps such as running the command npm config set registry ?
  3. What happens if you mistakenly removed or changed your .npmrc configuration to not include the internal private npm registry?

In any of these cases, where the custom setting for an internal registry was omitted, the npm package manager will default to the public registry (registry.npmjs.org) and will download packages from that.

Anyone can publish packages on the public npm registry, and so, if a malicious user were to publish a package named superlaser, then it would’ve been downloaded and installed instead of your own internal package.

How to protect against npm dependency confusion

The core issue resides with the concern of not having the proper private npm proxy configuration. If a developer, or a CI system, misses on having this configuration, then you’re potentially vulnerable.

So the first step is: Always ensure that a .npmrc file is made available, or another form of the private npm proxy configuration.

Secondly, you can take a proactive approach that detects cases in which you are using private packages that have their namespace unreserved on the public npmjs registry. We built snync to help you with that. You can run it in a CI server as part of the steps, before you actively install dependencies. This way, it can protect you from mistakenly installing a malicious package.

In the following screenshot, I am running snync vianpx and providing it with the current directory to scan for dependencies, as well as specifying that the package named superlaser is indeed a private package:

blog-dependency-confusion-snync.jpg

As you can see in the results, snync confirmed for me two particular cases of potential issues of dependency confusion:

  1. The death-star-secret-hyper-matter-reactorpackage is vulnerable because there’s no package of this name that is registered at the moment on the public npmjs registry. It means that  anyone can register it and then a dependency confusion attack can take place.
  2. The superlaser package is suspicious. This means, that the tool detected one of two cases:
    1. This package name was first introduced to the Git source code, and only later in time, a package of the same name was published to the public npmjs registry. This doesn’t mean that the public package on the npmjs registry is malicious, but warrants a review.
    2. The package name already exists on the public npmjs registry, even before you created a package of the same name as a private one.

snync is an open source Node.js-based command line project, and we invite you to use it for your DevSecOps security pipeline.

Private npm registry fetches latest versions

What happens if there’s a package of the same name of ours (superlaser) that is published and available in the public npm registry, but has a higher semver version?

To illustrate, the situation is as follows:

  • [email protected] exists in private npm registry https://localhost:4783
  • [email protected] published by anonymous user to public npm registry at https://www.npmjs.com/package/superlaser

Now the question is, what happens if a new project is scaffolded and requests to install the superlaser package? There’s no package.json yet, there’s no lock file yet (package-lock.json). A developer simply starts off with:

npm install superlaser

This install potentially ends with a malicious version of superlaser which a remote attacker controls. But why? The developer has the local npm registry configured.

As testing shows, even if an internal npm private proxy is configured, it has been observed that the behavior of many of these proxies is to first check the newest version available in the npm public registry. If such a newer version exists, these proxies fetch the newest semver version of the package from the public registry and install that.

Let’s replicate this scenario with Verdaccio. As you can see below, I have pushed the harmless superlaser npm package to Verdaccio, which serves my purposes as an internal hosting of private npm packages:

verdaccio npm private proxy server

Next, I’ll show you how in a new project directory that only has the.npmrc file pointing to the local Verdaccio registry, an npm install command for the superlaser package fetches the latest version from the public npm registry, even though I was expecting that I will just get [email protected] which is what I have published internally:

npm install fetches the newest version from the public npmjs registry

This creates an unexpected result and may potentially put end-users at risk.

Note, there is a public discussion in the open source Verdaccio project at GitHub about this behavior if you wish to participate and follow-up on this topic.

Technically, this process taken by Verdaccio and other private npm proxies, takes into account several variables, such as:

  1. The latest semver release
  2. The time the package was published

So, for example, if a high semver version exists on the public npmjs registry, yet a package of the same name with a lower semver version (of the same range as the public one) is created after the publish date of the high version, then Verdaccio will not fetch the public registry package.

How to protect against fetching the wrong package

Configure your private npm proxy to never proxy requests upstream to the public registries. If a package or version is not available locally, it should be resolved in a way that doesn’t blindly fetch packages from untrusted and unvetted sources.

If you are using Verdaccio, like in our examples here, you can do it like so with the following configuration that resides in /verdaccio/conf/config.yaml:

#
# This is the config file used for the docker images.
# It allows all users to do anything, so don't use it on production systems.
#
# Do not configure host and port under `listen` in this file
# as it will be ignored when using docker.
# see https://verdaccio.org/docs/en/docker#docker-and-custom-port-configuration
#
# Look here for more config file examples:
# https://github.com/verdaccio/verdaccio/tree/master/conf
#

# path to a directory with all packages
storage: /verdaccio/storage/data
# path to a directory with plugins to include
plugins: /verdaccio/plugins

web:
  # WebUI is enabled as default, if you want disable it, just uncomment this line
  #enable: false
  title: Verdaccio
  # comment out to disable gravatar support
  # gravatar: false
  # by default packages are ordercer ascendant (asc|desc)
  # sort_packages: asc
  # darkMode: true

# translate your registry, api i18n not available yet
# i18n:
# list of the available translations https://github.com/verdaccio/ui/tree/master/i18n/translations
#   web: en-US

auth:
  htpasswd:
    file: /verdaccio/storage/htpasswd
    # Maximum amount of users allowed to register, defaults to "+infinity".
    # You can set this to -1 to disable registration.
    # max_users: 1000

# a list of other known repositories we can talk to
uplinks:
  npmjs:
    url: https://registry.npmjs.org/

packages:
  '@*/*':
    # scoped packages
    access: $all
    publish: $authenticated
    unpublish: $authenticated
    # DO NOT FETCH PACKAGES FROM NPMJS
    #proxy: npmjs

  '**':
    # 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
    # DO NOT FETCH PACKAGES FROM NPMJS
    #proxy: npmjs

middlewares:
  audit:
    enabled: true

# log settings
logs:
  - { type: stdout, format: pretty, level: http }
  #- {type: file, path: verdaccio.log, level: info}
#experiments:
#  # support for npm token command
#  token: false
#  # support for the new v1 search endpoint, functional by incomplete read more on ticket 1732
#  search: false

# This affect the web and api (not developed yet)
#i18n:
#web: en-US

The above is the stock Verdaccio configuration file for the Docker container version of it, except that you can locate the # DO NOT FETCH PACKAGES FROM NPMJS comment which on the next line comments the proxy: npmjs option. This blocks Verdaccio from fetching anything from npmjs for the packages set by the matching pattern.

Manual package updates may introduce malicious versions

In this scenario, you are manually updating your npm packages by running npm update or npm install <packages>@latest to bring your dependencies versions up to date.

When you invoke these update procedures, then the same behavior that we witnessed before takes place here too. The npm update command asks the private npm proxy to fetch the latest version, which in turn, the proxy checks for the most up-to-date version on the public npm registry.

Note, if you’re a yarn user, then issuing a yarn upgrade will yield the same result of pulling in the potentially malicious packages from the public npmjs registry.

We can demonstrate it in the following scenario, where we start off with the internal [email protected] version:

{
  "name": "new-project",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "superlaser": "^1.0.0"
  }
}

Now, I do have a .npmrc file which defines the local registry and is pointing to the Verdaccio server I have running. Yet, if I simply run npm update to bring all of my dependencies up to date, you can see it pulls in the latest semver matching version from the public npmjs registry:

npm update

How to protect against it?

Instead of manual and blind npm package updates, opt-in for automated package updates in the form of pull requests raised to your open source project repositories, which will also take care of syncing the package manifest (such as package-lock.json or yarn.lock).

Snyk is one way you can freely automate npm package updates, as the following screenshot of a merged pull request shows:

automating npm package updates with Snyk

You can further fine-tune the automated update settings, such as limiting the number of pull requests that Snyk will open, or to completely ignore updating specific packages. There’s more on that in Snyk’s documentation on upgrading dependencies with automatic PRs.

Prevent dependency confusion with Snyk

Keep your applications safe from supply chain security vulnerabilities for free.

Concluding with application security resources

If you made it this far along, you’ve earned the answer to the quiz we presented at the beginning of the article. We named the open source tool snync as an abbreviation for So Now You’re Not Confused. Did you get it?

Practicing secure methodologies, whether as writing code, or day to day developer security hygiene, can’t be stressed enough these days with supply chain security incidents. If you or your team are doing JavaScript or Node.js regularly then you’ll find these reference resources useful:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK