Dependency Confusion Attack
debt(d5/e5/b5/t7)
Closest to 'specialist tool catches it' (d5). The term's detection_hints.tools lists semgrep, snyk, and dependabot — these are specialist security scanners that can detect missing private registry pinning or namespace scoping issues. Standard linters won't catch this; it requires supply chain security tooling.
Closest to 'touches multiple files / significant refactor' (e5). The quick_fix mentions setting private registry configuration, using namespace scoping for internal packages, and adding repository forcing in composer.json. While each individual fix is small, remediating across a codebase requires updating package manager configs, renaming internal packages to use vendor namespaces, and potentially modifying CI/CD pipelines — a significant cross-component effort.
Closest to 'persistent productivity tax' (b5). The term applies to all PHP contexts (web, cli) and the fix requires ongoing discipline: every new internal package must use proper namespacing, registry configs must be maintained, and developers must understand the resolution rules. This creates a persistent process tax that shapes how all internal packages are created and managed.
Closest to 'serious trap - contradicts similar concept elsewhere' (t7). The misconception field states developers believe private registries are safe because they are not public, but the attack exploits package manager resolution order — public registries are checked even for private package names. This directly contradicts the intuitive mental model that 'private means isolated,' making the obvious assumption dangerously wrong.
Also Known As
TL;DR
Explanation
Alex Birsan's 2021 research showed that when a company uses private packages (e.g. company-utils on a private registry), an attacker can publish company-utils on the public npm/PyPI/Packagist with a higher version number. Many package managers check public registries first or alongside private ones — the higher-versioned malicious package gets installed instead. Mitigations: namespace all private packages (e.g. @company/utils), configure the package manager to only use the private registry for internal packages, and pin exact versions.
Common Misconception
Why It Matters
Common Mistakes
- Internal Composer packages without a vendor namespace — company-utils is registerable on Packagist.
- Package manager configured to check both private and public registries without preference rules.
- Not pinning exact versions in composer.lock — allows higher malicious versions to resolve.
- Not scanning composer.lock for unexpected public packages matching private names.
Code Examples
// composer.json with unnamespaced private package:
{
"require": {
"internal-utils": "^1.0" // Attacker publishes internal-utils 99.0 on Packagist
},
"repositories": [
{"type": "composer", "url": "https://private.registry.example.com"}
]
// Public Packagist also checked — malicious v99 wins!
}
// Namespaced + private-only resolution:
{
"require": {
"company/internal-utils": "^1.0" // Namespaced — harder to squat
},
"repositories": [
{"type": "composer", "url": "https://private.registry.example.com",
"only": ["company/*"]} // Only fetch company/* from private registry
],
"config": {
"secure-http": true
}
}