A dependency declaration can look precise and still leave an important security question unanswered: where is this package allowed to come from?
This matters when an organisation uses both private packages and a public package registry. If a package manager or build configuration can resolve the same package name from more than one source, an attacker may be able to publish a public package that competes with the intended private one. A build that selects the wrong source can then run attacker-controlled package code inside a trusted development or build environment.
This class of supply-chain problem is commonly called dependency confusion. The central defensive lesson is broader than any package manager: a dependency identity should include its trusted source, not only its name and version.
This article explains that mental model, shows where ambiguity enters dependency resolution, and develops practical controls for keeping private and public package namespaces separate.
A package name is not a complete trust decision
Suppose an internal service depends on a package named company-auth-utils.
A simplified dependency file might express only this:
company-auth-utils = 4.2.0That line answers two questions:
- which package name the application wants;
- which version it expects.
It does not necessarily answer a third question:
Which registry is trusted to provide company-auth-utils?Whether that question is already answered depends on the package ecosystem and the project’s configuration. Some setups associate packages or namespaces with a particular registry. Others search or consider multiple configured sources according to ecosystem-specific rules.
The security problem appears when the intended source is private but the resolver is also willing to accept a package with the same identity from an untrusted or less-trusted source.
A useful model is:
requested dependency
|
v
name + version + trusted source
|
v
resolved packageIf the trusted-source part is implicit or ambiguous, the resolver may make a choice that the developer did not intend.
How dependency confusion crosses a trust boundary
Consider a company with two package sources:
private registry: packages.corp.example
public registry: public ecosystem registryThe private registry contains company-auth-utils. Developers expect builds to obtain that package only from the private registry.
Now assume the build environment is also configured to use the public registry. If the resolver can consider a public package named company-auth-utils, the package name has crossed a trust boundary: two different publishers can potentially supply something that the build treats as the same dependency identity.
The exact selection behaviour is package-manager specific. A resolver might prefer one source, compare versions across sources, use source order, or reject ambiguity. Do not assume a universal rule. The defensive requirement is to configure the ecosystem so that a private dependency cannot silently fall back to an unintended public source.
If an attacker can cause their package to be selected, the impact is not limited to incorrect application behaviour. Package installation and build processes may execute scripts, plugins, generators, or other package-controlled code. The resulting code may also be compiled or bundled into the application. The practical consequence depends on the ecosystem and build privileges, but the trust decision happens before the application reaches production.
Make the allowed source explicit
The strongest general control is to define which source is authoritative for each private package or namespace.
For ecosystems that support scoped namespaces, reserve an organisation-controlled scope and map that scope to the private registry. Conceptually:
@company/* -> private registry only
other packages -> approved public sourceFor ecosystems without that exact mechanism, use the closest supported equivalent: source mapping, repository declarations, package-source rules, or separate resolver configuration that binds internal package identities to an internal source.
The important property is not the configuration syntax. It is the resulting invariant:
A package intended to be private cannot be satisfied by a package published to an unrelated public registry.
Test that invariant rather than assuming the configuration expresses it.
Do not treat source order as a strong boundary
A common design is to configure the private registry first and the public registry second:
1. private registry
2. public registryThis can be useful when the package manager documents strict source priority and the configuration is enforced consistently. But source order alone is a fragile security boundary when resolver behaviour is more complicated.
For example, some dependency tools may consider versions from multiple sources rather than simply stopping at the first source containing a matching name. Tool behaviour can also change with configuration, plugins, or ecosystem conventions.
Prefer a rule that says this package may come only from this source over a rule that merely says try this source first.
If strict source binding is unavailable, reduce ambiguity in other ways and document the remaining assumption. Possible controls include isolating private resolution behind an internal proxy, restricting which upstream registries the proxy may use, and testing resolver behaviour in continuous integration.
Use an internal package proxy deliberately
Many organisations route dependency downloads through an internal repository or proxy. This can centralise policy, caching, availability, and audit information, but a proxy does not automatically remove dependency confusion.
A proxy that combines private packages with public upstream packages still needs a rule for name collisions. Ask what happens when the same package identity exists internally and upstream.
A defensible design establishes an ownership boundary. For example:
internal namespace -> private hosted repository
approved external packages -> proxied public repositoryThe proxy should not fetch an external package as a substitute when a name belongs to the internal namespace.
This also makes failure behaviour important. If the private repository is temporarily unavailable, silently falling back to a public package with the same name trades availability for integrity. For private identities, failing the build is generally safer than changing the package’s trust source without an explicit decision.
Lockfiles help, but they solve a different problem
Lockfiles are valuable because they record a resolved dependency graph and often include exact versions and integrity information. They improve reproducibility and can reduce unexpected dependency changes.
They should not be treated as the only dependency-confusion defense.
A lockfile may protect an existing, correctly resolved dependency while still leaving other paths exposed: adding a new dependency, regenerating the lockfile, updating packages, building a component without the lockfile, or using a tool that does not record source identity strongly enough for the threat model.
The two controls answer different questions:
source policy: Who is allowed to supply this dependency?
lockfile: Which resolved dependency should this build reproduce?Use both when the ecosystem supports them. Source policy establishes trust; the lockfile helps preserve a reviewed resolution.
Treat package-source configuration as security-sensitive
Once source mapping protects the trust boundary, changes to that mapping deserve review.
A small configuration edit can have a large effect. Adding a public source, removing a namespace mapping, changing an internal proxy, or enabling fallback behaviour can alter where executable dependency code originates.
Keep package-source configuration in version control when practical. Review it alongside dependency manifests and lockfiles. In build systems, prefer centrally managed configuration over undocumented developer-machine settings that may resolve the same project differently.
Also control credentials narrowly. A build that needs read access to a private package repository usually does not need permission to publish packages or administer the repository. Least privilege does not stop dependency confusion by itself, but it reduces the damage if a build environment is compromised through any supply-chain path.
Verify the control with negative tests
A useful security control should be testable.
For a private package identity, create a safe test that demonstrates the resolver will not obtain that identity from the public source. The exact method depends on the ecosystem, but the expected result should be clear:
private source unavailable
+
public source has matching name
-> resolution failsThe test should not publish deceptive packages to a real public registry. Use an isolated test registry, repository fixture, or package-manager test environment under your control.
Also verify clean builds, because local caches can hide source-resolution mistakes. A developer whose machine already contains the correct package may see a successful build even though a fresh build agent would resolve differently.
Monitor changes to package-source configuration and investigate unexpected source changes in lockfiles or dependency metadata where the ecosystem records them.
Understand what source binding does not solve
Explicit package sources reduce one specific risk: selecting a package from an unintended registry because package identity is ambiguous.
They do not prove that the trusted registry itself is trustworthy at every moment. They do not protect against:
- compromise of an authorised package publisher;
- malicious changes to a legitimate internal package;
- stolen publishing credentials;
- vulnerable but authentic dependencies;
- a compromised registry or build service;
- unsafe install scripts in packages that were intentionally approved.
Those risks need complementary controls such as protected publishing workflows, multi-factor authentication for maintainers where supported, dependency review, vulnerability management, provenance or signature verification where available, restricted build privileges, and monitoring of package changes.
Source binding is therefore a boundary control, not a complete software-supply-chain strategy.
Choose controls according to the package environment
A small project that consumes only public packages from one well-defined registry may not need private namespace rules. Its main concerns are more likely to be dependency review, version control, integrity, and maintainer compromise.
The dependency-confusion threat becomes important when a build can obtain packages from multiple trust domains, especially when private package names coexist with public registries.
In that environment, make the trust relationship explicit:
private identity -> private source
public identity -> approved public source or controlled proxy
ambiguous source -> build failureThe precise syntax varies by ecosystem, but the security decision should not.
Conclusion
Dependency confusion is fundamentally an identity problem. A package name and version describe what a build wants, but they may not fully describe who is trusted to supply it.
Bind private package identities or namespaces to their intended registry, avoid relying on ambiguous fallback behaviour, use lockfiles as a complementary reproducibility control, and test resolution from a clean environment. When a private source is unavailable, failing the build is usually a better outcome than silently changing the source of trusted code.
The practical rule is simple: make package origin part of the dependency trust decision.