What is dependency confusion?

Direct answer

It's when an attacker publishes a package to a public registry using the name of one of your internal packages, at a higher version number. Package managers configured to check both registries pick the public one. The fix is scoped names and a private registry that doesn't fall back to public.

Muhammad HasanUpdated

You have an internal package, acme-auth-utils, published to a private registry. Nobody outside your company can install it.

An attacker publishes acme-auth-utils to the public npm registry at version 99.0.0. Your build machine, configured to check both, resolves the higher version. From the public registry, and runs its install script.

The attacker never needed access to anything. They needed the name.

Where names leak

More easily than you'd expect. Package names appear in committed lockfiles, in package.json of open source projects that reference internal tooling, in stack traces posted to issue trackers, in job adverts, in conference talks, and in source maps shipped to browsers.

Assume your internal package names are public.

Scoped packages

The main defence on npm. A scoped name is tied to an organisation:

"@acme/auth-utils": "^1.2.0"

Nobody else can publish under @acme once you own it. Register the scope on the public registry even if you never publish there. That's the point.

@acme:registry=https://npm.acme.internal

Scope-specific registry configuration means @acme/* resolves privately and everything else goes public, with no ambiguity.

Don't let the registry fall back

The underlying problem is a resolver checking two sources and picking by version. Configure your private registry so internal names never fall through to public.

Artifactory, Nexus and most hosted equivalents support exclusion patterns for exactly this. Set them.

Pin and verify

Commit lockfiles and install with npm ci rather than npm install, so resolution doesn't happen fresh in CI.

Disable install scripts where your build doesn't need them (--ignore-scripts), since that's the usual execution vector.

Other ecosystems

Python, pip with --extra-index-url has the same behaviour, and it's arguably worse because there's no scoping equivalent. Use --index-url pointing at a private index that proxies public packages, rather than adding a second index.

Ruby. source blocks in the Gemfile scope which gems come from where.

Java / Maven, repository ordering and mirror configuration; less exposed by default but worth checking.

Reserve the names

Cheap insurance: publish placeholder packages under your internal names to the public registry. Nobody can then squat them.

Slightly awkward, occasionally confusing to outsiders, and it closes the hole permanently for names you can't rename.

Detection

Audit your lockfiles for internal-sounding names resolving to public registry URLs. That's the signature, and it's a quick grep.

A sudden major version jump on an internal package is worth investigating rather than merging.

Go deeper

Kolega for DevOps

Related answers

See what your own repository returns

Connect a repo and run a scan. No credit card, no pipeline changes.

Get started for free