Your company has an internal package called acme-shared-utils, published to a private registry, at version 1.4.2. It is not on the public registry.
Someone publishes acme-shared-utils version 99.0.0 to the public registry. Your package manager, configured to check both, sees a higher version publicly and installs it. Their code now runs in your build with whatever access your build has.
The attack requires no compromise of your systems. It requires knowing an internal package name, which frequently leaks through public code, error messages, or job listings.
What changed in 2026
- Namespace reservation became standard practice. Reserving an organisation namespace on public registries became routine.
- Registry configuration improved. More package managers gained explicit controls for which registry serves which package.
- Lockfile integrity verification spread. Cryptographic hashes in lockfiles became widely enforced rather than optional.
- Install-script scrutiny increased. Restricting or auditing lifecycle scripts became a common policy.
Why version resolution causes it
The root cause is that many package managers resolve a name across all configured registries and select by version rather than by source.
That is reasonable default behaviour for public packages and dangerous when the same name exists in two places. An attacker who publishes a very high version wins the resolution.
| Configuration |
Vulnerable? |
| Checks public and private, picks highest version |
Yes |
| Private registry with fallback to public on miss |
Yes, if the name is missing internally |
| Explicit mapping of names to registries |
No |
| Reserved namespace on the public registry |
No |
| Lockfile with pinned source and hash |
No, for existing dependencies |
The second row catches people who believe a private registry protects them. If the private registry returns a miss — a typo, a package not yet published, a transient failure — and the configuration falls back to public, the substitution happens.
Namespaces are the strongest defence
Publishing internal packages under a namespace your organisation controls on the public registry means nobody else can publish under it. The name simply cannot be squatted.
Where the ecosystem supports scoping, this is the cleanest structural fix: reserve the namespace publicly, publish internal packages under it privately, and the ambiguity disappears.
Where scoping is unavailable, the equivalent is explicit registry mapping — configuring which registry serves which name prefix, so internal names are never resolved publicly regardless of what exists there.
Some organisations also publish placeholder packages publicly under their internal names, occupying the namespace defensively. Crude and effective where scoping is not an option.
Lockfiles and integrity
A lockfile records exactly which version came from which source, with a cryptographic hash of its contents.
That protects existing dependencies: if a package's contents change, or a different package is substituted, the hash mismatches and installation fails. This is the mechanism that stops silent substitution.
Two requirements. The lockfile must be committed and used — an install that ignores it or regenerates it defeats the protection. And builds should use the strict install mode that fails on mismatch rather than resolving fresh.
The gap the lockfile does not cover is a new dependency being added. Its first resolution has nothing to compare against, which is where namespaces and registry mapping matter.
Install scripts
Worth stating explicitly because it explains the urgency: many ecosystems allow packages to run scripts at install time, before any of your code executes and before tests run.
So a substituted package does not need you to import it. Installing it is enough, and it runs with whatever access your build environment has — which frequently includes credentials, cloud access, and the ability to modify build artefacts.
Restricting or disabling lifecycle scripts, where your workflow tolerates it, removes a large part of the attack surface. Where it does not, auditing which dependencies use them narrows the concern to a reviewable set.
Common mistakes
- Assuming a private registry is sufficient. Fallback behaviour matters.
- No namespace reservation. The name stays available to anyone.
- Lockfile not committed or not enforced. No integrity check.
- Install mode that resolves fresh. Ignores the lockfile.
- Internal package names in public code. Discloses the target.
- Install scripts unrestricted. Compromise before execution.
- No alert on dependency source changes. Substitution goes unnoticed.
FAQ
How would I know if this happened?
A dependency resolving from an unexpected registry, an unexpected version jump, or an integrity mismatch. Without lockfile enforcement and source logging, quite possibly you would not.
Does this affect internal-only repositories?
Yes — the attack targets the build, not the repository. Any build resolving package names across registries is exposed.
What about transitive dependencies?
Same risk, less visibility. A dependency of a dependency referencing an internal name has the same exposure, which is an argument for registry-level controls rather than per-project configuration.
Is this the same as typosquatting?
Related. Typosquatting relies on a developer mistyping a name; dependency confusion relies on resolution preferring a public package with a name you already use correctly.
Where to go next
For the build caching that must trust its inputs, read incremental builds. For the wider pipeline, CI/CD best practices, and for verifying what you ship, API fuzzing.