Dependency confusion hides inside a name. A manifest asks for something called internal-utils, the resolver takes the highest version it can find, and the copy it finds is not yours: a public package carrying the same name wins because its version number is bigger. No one phished you and no maintainer was compromised. Someone read a name out of your repository and registered it. Below: how a dependency confusion attack is carried out, what separates it from typosquatting plugin names, where private registry hijack fits in the same family, and the plugin name impersonation signals you can actually verify before installing.
How a dependency confusion attack runs
The steps are dull, which is part of why nobody catches them. Someone reads a public repository, finds a private dependency named in a manifest, and registers that exact name on the public registry with a version number high enough to win any comparison. The next time a build resolves dependencies without a scope or a registry pin, it pulls the public copy instead of the internal one. Nothing in the diff looks wrong, because the manifest line never changed.
- Internal names are guessable. They follow naming conventions that leak through documentation, stack traces and example configs.
- Version resolution usually prefers the highest number available, so an attacker controls the outcome by publishing something absurd like 99.0.0.
- Code review does not help here. The change you would be reviewing is not in your repository.
Typosquatting plugin names is the cheaper cousin
Typosquatting plugin names needs none of that reconnaissance. The attacker takes a popular name and registers near-misses: transposed letters, a hyphen where you expect an underscore, a plausible-looking prefix. The economics favour them, since one mistyped install command is enough and the person mistyping is usually in a hurry.
| Aspect | Dependency confusion | Typosquatting |
|---|---|---|
| What gets abused | A real private name that already exists | A misspelling of a public name |
| Who gets hit | Teams running a private registry | Anyone typing quickly |
| Where it is caught | Registry config and scope rules | A name check before install |
| Cleanup afterwards | Purge caches, re-pin, rotate secrets | Remove the package, check what it did |
Both end the same way: software you did not choose runs on machines you are responsible for. Only the entry point differs, so the two are worth defending against together rather than separately.
Private registry hijack sits between them
A private registry hijack is the variant people forget, because nothing about the plugin looks wrong. The namespace existed legitimately for years, then ownership lapsed: expired payment, an abandoned maintenance account, a domain transfer nobody watched. A new party takes control of a name other people already depend on, and the package keeps working exactly as before, which is what makes it hard to notice.
Plugin name impersonation signals worth checking
Three minutes with a name rules out most of this. What you are looking for is a mismatch between how established a plugin appears and how thin its actual history is.
| Signal | Healthy | Suspicious |
|---|---|---|
| Publisher vs repo owner | Same org on the index and the source | Index name appears nowhere in the repo |
| First publish date | Years of releases | Recent publish, widely referenced anyway |
| Namespace | Scoped, such as @acme/tool | Unscoped generic noun |
| Install behaviour | Reads bundled files | Fetches from a host registered recently |
On a public registry a name is a claim, not an identity. Verify the thing behind the name rather than the name itself.
Where the fixes belong
This is one of the few supply chain problems with cheap mitigations, and they live in four different places. You want all four, because each one covers a case the others miss.
- Registry side: claim your prefixes and enable namespace protection, so nobody can register anything matching them.
- Resolver side: disable the behaviour that lets a public source satisfy a name you normally pull privately.
- CI side: fail the build when a manifest adds a name whose owner does not match a known publisher, instead of printing a warning.
- Index side: check the entry against the source repository before installing, using the same checks described in /blog/how-to-avoid-risky-dsh-plugins.
FAQ
- Is dependency confusion still common? Less than at its peak, because large registries now offer namespace protection and most package managers default to scoped installs. It survives in smaller ecosystems and internal mirrors.
- Does version pinning solve it? Partly. Pinning stops the surprise upgrade but not the first wrong install. Pinning the source registry is the part that closes it.
- How is this different from a compromised maintainer? A compromised maintainer is a person problem; this is a naming problem. The code was never trustworthy from the first publish.
- What should I do today? Claim your prefixes on the public registry you use, then check whether any manifest entry resolves from somewhere you did not intend.
All three variants start in the name field, so that is where to spend your attention first. The related naming games are covered in /blog/tag-baiting-problem, per-plugin red flags are listed in /blog/how-to-avoid-risky-dsh-plugins, team-wide enforcement with an allowlist is described in /blog/plugin-supply-chain-security-team-enforcement and /blog/setting-up-a-plugin-allowlist-for-your-dev-team, registry-side scanning in CI is at /blog/ci-cd-plugin-scanning, and the full plugin index is at /.