Your project depends on an internal library — something like acme-billing-utils — published to a private registry. Nobody outside the company has ever seen it. That name is still available to anyone on the public registry, and that is the whole attack.

It is one of five routes into a software supply chain, and among the easiest to close once you can see it. The mechanism is a resolution rule, not a bug in any individual tool. Most package managers can be configured with more than one source: a private registry for internal packages, and the public one for everything else. When a package is requested, they check the available sources — and in a range of common configurations, a higher version number wins regardless of which source it came from.

So an attacker who learns the name acme-billing-utils publishes a package by that name to the public registry, with a version like 99.0.0. Your next build resolves the internal name, sees a much higher version available publicly, and installs the attacker's code — which typically runs at install time, on a build server, with credentials in the environment.

Nothing was compromised to make this work. No password was stolen. The attacker only needed a name.

The technique was demonstrated publicly by Alex Birsan in February 2021, against Apple, Microsoft, PayPal and dozens of others, using nothing more than package names harvested from public sources. Every one of those builds resolved the higher version from the public registry.

Python deserves a specific mention, because the trap is in a flag people copy without thinking. pip install --extra-index-url adds an index without giving it lower priority: pip queries both and takes the highest version, which is precisely the vulnerable behaviour. --index-url replaces the default instead, and is the safe form.

Where the names leak from

It is worth being honest that internal package names are not secret in practice, even when everyone assumes they are. They appear in:

  • Committed lockfiles and manifests in a repository that later becomes public, or that was always public because only some of the company's code is closed.
  • Bundled front-end assets, where a source map or an unminified build ships the names of internal modules to every visitor.
  • Public CI logs — build output on a public repository prints the packages being installed.
  • Job adverts, conference talks and support forums, where a stack trace gets pasted verbatim.

The practical conclusion is that a defence built on the name staying private is not a defence.

How to detect exposure in your own project

Most descriptions of this attack get the detection backwards. They present it as something you can only check once you have listed your internal package names, which makes it sound like an audit requiring onboarding and configuration.

It does not. The check is simpler and works from the outside: take every direct dependency your project declares, and ask the public registry whether that name exists.

A dependency your build requires, whose name returns "not found" from its public registry, is by definition a name that resolves from somewhere else — an internal registry, a local path, a mirror. That unclaimed public name is the exposure, whoever owns it. An attacker can register it, and the only thing standing between you and their code is your resolution configuration.

Two refinements make the result far more useful:

A private registry configured in the repository raises confidence. If the project contains an .npmrc pointing at an internal registry, a Maven settings file with a custom repository, or similar, then the precondition for the attack is demonstrably present, not hypothetical.

Workspaces are the innocent explanation. In a monorepo, packages reference each other by name and are resolved locally — they will also 404 publicly, and they are fine. Detecting a workspace configuration and saying so turns a confusing finding into an obvious one.

How to actually close it

Three defences, in descending order of how well they work:

Use a scoped or namespaced prefix that you own. On npm this means publishing under an organisation scope such as @acme/billing-utils and owning @acme publicly. An attacker cannot publish into a scope you control. This is the strongest fix because it removes the ambiguity rather than managing it.

Claim the names defensively. Publish placeholder packages under your internal names on the public registry. Crude, effective, and it costs nothing.

Configure resolution explicitly. Pin internal names to the internal registry so the public one is never consulted for them. This works, but it depends on configuration staying correct across every project and every new developer's machine — which is exactly the kind of thing that drifts.

Whichever you choose, the useful posture is to assume the names are already known and make the name insufficient.

What is dependency confusion?

Dependency confusion is a supply-chain attack where an attacker publishes a package to a public registry using the name of one of your internal, private packages. Because many build configurations consult both a private and a public source and prefer the highest version number, your build can resolve the attacker's package instead of yours and run their code — typically at install time, on a build server. No credentials need to be stolen; the attacker only needs to learn the name.

How do I know if my project is exposed to dependency confusion?

Take every direct dependency your project declares and ask the public registry whether that name exists. A dependency your build requires whose name returns not-found from its public registry is resolving from somewhere else, which means the public name is unclaimed and an attacker could register it. That unclaimed name is the exposure. A private registry configured in the repository makes it more serious, while a monorepo workspace configuration is the common innocent explanation.

How do I prevent dependency confusion?

The strongest fix is to publish internal packages under a namespace or scope you own publicly, such as an npm organisation scope, because an attacker cannot publish into a scope you control. Defensively registering placeholder packages under your internal names on the public registry also works and costs nothing. Configuring your package manager to resolve internal names only from the internal registry works too, but depends on that configuration staying correct across every project and machine.

Are internal package names actually secret?

Internal package names are rarely secret in practice. They leak through committed lockfiles in repositories that later become public, through source maps and unminified front-end bundles shipped to every visitor, through build output in public CI logs, and through stack traces pasted into support forums or conference talks. Any defence that depends on the name staying unknown should be treated as no defence at all.

Where CodeControl fits

CodeControl checks every direct dependency against its public registry and raises a finding for any name that does not exist there — no configuration or onboarding required, because the unclaimed name is the exposure. A private registry configured in the repository raises the severity, and npm workspaces are detected so the innocent monorepo explanation is offered rather than buried. It runs alongside typosquatting and detection of AI-hallucinated package names.