Dependency confusion happens when a public package with the same name as your private one gets installed instead. In Composer 2, repositories are canonical by default, so the first repository that has a package wins. The only and exclude options narrow that further.
How Composer picks a repository
The Composer docs explain that it looks a package up in the topmost repository and stops at the first one that contains it. Packagist.org is added implicitly as the last repository unless you disable it. In Composer 1.x all repositories were non-canonical, so Composer kept searching. In 2.x all are canonical by default.
The attack the docs sketch is simple. If your private repository were non-canonical and someone published a higher version of the same name on Packagist, Composer could prefer the public one. With canonical behaviour, a higher version from Packagist is not even considered when your private repository has that name.
The three controls
| Control | What it does | Use it when |
|---|---|---|
| Order of repositories | Higher entries are checked first | Always, put private sources above Packagist |
| canonical set to false | Keep looking in lower repositories | Rarely, and with care |
| only | A repository may serve just the listed names | You want a strict allow list per source |
| exclude | A repository must not serve the listed names | You need to block specific names from a source |
Both only and exclude accept arrays of names, including wildcards such as a whole vendor prefix.
Steps to harden a mixed setup
- List the vendor prefixes you own, for example your company name.
- Put your private repository above Packagist in composer.json.
- Add an only entry on the private repository for your prefixes.
- Add an exclude entry on Packagist for those same prefixes, so a public package with your name cannot be served.
- Reserve your vendor name on Packagist. The Packagist about page says vendor names are protected once a package with that name has been published.
- Test by trying to require a package with your prefix that does not exist privately. It should fail, not fall through.
Mistakes to avoid
Do not set canonical to false just to pick up a fork with higher versions without understanding the consequence. The docs say non-canonical use is mainly for a source preferred when it has higher versions, and that is the setup a public attacker package would exploit. Also avoid mixing in many extra repositories. Each is another place a name can be served from.
Try it yourself
Click through it at your own pace, or open it full screen. Check where each package in the inventory came from.
Where Safeguard fits
Safeguard's reachability analysis helps separate advisories your code can reach from those it cannot. You can try a free scan on your own lock file.
Quick check
Is dependency confusion possible with Composer 2 defaults?
It is much harder, because repositories are canonical by default. You can still weaken it by setting canonical to false.
Does protecting my vendor name on Packagist help?
Yes. Packagist protects vendor names once a package with that name exists, so others cannot publish under it.
Sources
Last reviewed: 19 October 2026.