Why does a dependency confusion attacker publish a package with an absurdly high version number?
answer
- trust is not part of the ordering
- two sources, one candidate list
- highest satisfying version wins
- prereleases are usually skipped
- you cannot out-bump an attacker
basics
~20 sBecause when a build consults an internal source and a public one for the same name, precedence is decided by version-selection rules, not by which source is trusted. The highest satisfying version wins, so a huge public version beats the internal one.
solid answer
~50 sWhen a client is configured with an extra source rather than a replacement one, it queries every configured source for the requested name and merges the results into a single candidate list. Selection then runs the ordinary version rules over that list — highest version satisfying the requirement — and trust plays no part. Picture a .NET back office where a shared package id exists both on the internal feed and on the public gallery: the attacker registers the id publicly at 9000.0.0, and every build agent whose requirement is an open range installs the public copy on the next restore, handing the attacker whatever credentials that agent holds. High versions are chosen so the copy wins any range, and stable rather than prerelease because many selectors exclude prereleases by default. Bumping your internal version higher is not a fix — the attacker just goes higher again.
go deeper
Know the one-line mechanic: with two sources configured for one name, the build takes the highest version it finds, and the attacker can always publish a higher one. Do not answer that the private source automatically wins.
Explain the merged candidate set: every configured source is queried, results are combined, and ordinary version rules pick a winner with no notion of trust. Mention why attackers pick a high stable version rather than a prerelease.
Be able to argue why version-bumping and source ordering are both non-controls, and describe how exact pins narrow the window without closing it — including what happens to transitive requirements nobody reviews.
Frame it as a property of how the build is allowed to resolve names, not a package-by-package problem, and be ready to say what evidence would convince you an estate no longer has any name resolvable from two sources.
## Two sources, one candidate list The heart of dependency confusion is not a bug in a resolver. It is a configuration in which a client has been told about more than one place a package name may live, and a selection algorithm that was never designed to express trust. When a source is added as an *additional* source rather than as a *replacement*, the client asks every configured source what versions of `some-name` exist and merges the answers into one candidate set. From that point the name has no memory of where each version came from. The usual rules then apply: discard versions that do not satisfy the declared requirement, and take the highest of what remains. The resolver is behaving exactly as documented; it simply has no notion that one of those hosts is yours and the other is the open internet. So the attacker's job reduces to putting a version into that merged set which will always sort to the top: | What the build declares | Internal source has | Public source has | What installs | |---|---|---|---| | `^2.1.0` | `2.1.4` | `2.99.0` | the public `2.99.0` | | `>=1.0` | `1.4.2` | `9000.0.0` | the public `9000.0.0` | | `2.1.4` exactly | `2.1.4` | `2.1.4` | ambiguous — decided by the client's merge rules | ## Why a large number, and why stable A large number is chosen because the attacker does not know your declared ranges and wants one publish to beat all of them. A version far above anything plausible satisfies every open-ended range and every caret or tilde range at that major, without needing reconnaissance on your requirement strings. Stable rather than prerelease matters because most version selectors following semantic-versioning conventions exclude prerelease versions unless the requirement explicitly asks for one. A prerelease at `9000.0.0-beta` would be ignored by exactly the builds the attacker is targeting. ## Why 'ours is listed first' is not a defence Candidates commonly answer that the internal source is first in the configuration so it wins. Two problems. First, in the merged-source model, source order is not part of version selection at all — it may decide only tie-breaks or which host is asked first, not which version is preferred. Second, even in clients that do consult sources in order, the interesting question is what happens when the first source answers *not found* for that name, which is a different failure path. Relying on ordering means relying on an implementation detail that varies by ecosystem and by version of the client, for a security property. That is the definition of an ambiguity an attacker will find. ## Why out-bumping the attacker fails The reflex fix — set the internal version to `999.0.0` so nothing public can beat it — fails on three counts. There is no ceiling you own: the attacker publishes `1000.0.0` the same afternoon, and the version space is theirs to escalate in as easily as yours. It must hold forever, on every internal name, in every ecosystem, including the names nobody remembers. And it makes your own versioning meaningless, so real upgrades can no longer be expressed. You would be racing an anonymous publisher, in public, with no finish line. ## Exact pins narrow it; they do not settle it If the requirement names one exact version that exists internally, the attacker cannot win on ordering — but they can publish *that same version* publicly, and which copy is served then depends on the client's merge behaviour rather than on a decision you made. Pinning also only covers what you pinned: transitive requirements resolved from another package's metadata are frequently open ranges, and internal packages that depend on other internal packages are exactly where an unreviewed range hides. ## The shape of the answer Say the mechanism, not the anecdote: two sources consulted for one name, a merged candidate set, selection by version rather than by trust, and an attacker who only has to be higher. Then name the two wrong instincts — out-bumping and relying on source order — and explain why each is a race or an implementation detail rather than a control. What actually closes it is a build that can only ever resolve an internal name from an internal source, which is a build-configuration decision beyond the scope of the mechanism itself.
- Why doesn't raising the internal version to 999.0.0 solve it?Because the version space belongs to whoever publishes next. The attacker responds with 1000.0.0 for the cost of one upload, and you would have to keep winning that race forever, on every internal name, in every ecosystem. It also destroys your ability to use version numbers to mean anything.
- If a requirement pins one exact version, is the build safe?Narrower, not safe. The attacker can publish the same version number publicly, and which copy is served then depends on the client's merge behaviour rather than on anything you decided. Pins also cover only what you pinned — transitive requirements are often open ranges, and those resolve through the same merged candidate set.
- Does this apply to a name that only appears as a transitive dependency?Yes, and those are the dangerous ones. The same merged lookup runs for every name in the graph, including internal packages pulled in by other internal packages. Nobody reviews those requirement strings, so open ranges survive there long after direct dependencies have been tightened.
saying these in an interview costs you the question
- Just keep the internal version higher than anything public
- The private source is listed first, so it always wins
- The client prefers whichever source it authenticated to
- Only direct dependencies can be substituted this way
- The resolver is buggy — it should know which source is ours