Dependency Confusion: The Bağımlılık Karışıklığı Supply Chain Attack
When an attacker uploads your internal package name to a public registry with a higher version, the package manager mistakenly pulls it and the code runs. How dependency confusion works, why it happens in npm, pip and maven, and how to close it with scoped packages, a private registry and lockfiles.
Quick answer: Dependency confusion is when an attacker uploads your company's internal package name (for example
company-shared-lib) to a public registry (npm, PyPI, Maven Central) with a higher version number. Because the package manager defaults to "take the highest version" and queries the internal and public registries at the same time, during the build it pulls the attacker's public package instead of the internal one, and the code inside it runs at build/install time. Protection: put internal packages under a scope/namespace, configure the package manager to pull internal packages only from the private registry, reserve your internal names on the public registry in advance, use lockfiles and integrity hashes, and continuously audit dependencies with an SCA tool.
Most modern software is built from third party packages, and this is the most productive target for supply chain attacks. In 2021, security researcher Alex Birsan ran code inside the build systems of dozens of large companies using only package name guessing; the method became known as "dependency confusion" and is still a widespread flaw. This guide explains the mechanism, why it naturally arises in package managers, and how an organization can close it at the root.
How the attack works
Companies write internal packages used only in house and pull them from a private registry. The problem is that the package manager queries the internal registry and the public registry together and usually picks the highest version number as the winner.
Step by step: the attacker somehow learns the internal package name (a leaked package.json, open source contributions, error messages, job postings). Then they upload the same name to a public registry with an absurdly high version like 99.0.0. When the company's build system resolves the package, it sees the public 99.0.0 as newer than the real 1.4.2 in the internal registry and pulls it. The package's install script (npm postinstall, Python setup.py) runs at build time; now the attacker's code runs in your CI/CD environment, often with access to credentials and source code. We covered CI/CD pipeline security in detail in our CI/CD pipeline DevSecOps article.
Why it naturally arises in package managers
| Ecosystem | Default behavior | Cause of confusion |
|---|---|---|
| npm (Node.js) | an unscoped name is searched in both public and private | the higher version on public wins |
| pip (Python) | an extra index is added, both are searched | the higher version on public can be pulled |
| Maven/Gradle (Java) | repository order plus version resolution | higher version from the wrong repo |
| NuGet (.NET) | multiple feeds together | pull from the public feed |
The common root cause is the same: the package manager answers "if two sources have the same name, which one?" with version height, not security, and does not enforce an internal/external distinction. This is not a "vulnerability" but a default that can be misconfigured.
Protection: separate internal from external
The root solution is to guarantee that internal packages can never be pulled from the public registry. Layered measures:
First, scope/namespace. Put internal packages under a scope (in npm @company/package) and bind that scope only to the private registry; then they cannot upload a package to public with the same scope. Second, registry configuration: set the package manager to pull internal names only from the private registry (scope to registry mapping in npm, a single trusted index plus disabling the others in pip). Third, name reservation: reserve your internal package names (or scope) on the public registry in advance with an empty package so the attacker cannot take the name. Fourth, lockfile plus integrity hash: lock files like package-lock.json and poetry.lock pin the expected version and cryptographic hash; an unexpected source/hash stops the build. Fifth, SCA (software composition analysis): continuously scan dependencies and flag known vulnerabilities and suspicious packages. We examined SCA's place within SAST/DAST in our SAST, DAST, IAST and SCA selection matrix article.
CI/CD and build environment hardening
The real damage of dependency confusion is running code in the build environment, so the build environment must also be hardened. Build containers should be restricted in outbound network access (only to approved registries), the execution of install scripts (postinstall etc.) should be reviewed, and build secrets and tokens should be granted with the least privilege principle so that even if a package runs, what it can obtain is limited. Keeping an SBOM (software bill of materials) makes which version came from where auditable. For cloud based build pipelines our cloud penetration testing article and for the API surface our API security OWASP API Top 10 article are complementary.
The KAOS and DSET approach
At DSET, in supply chain assessments we examine whether the organization's internal package names have leaked externally (are they reserved on the public registry, can an attacker take them), whether the package manager configuration enforces the internal/external distinction, and whether the build environment is isolated. The KAOS tool we develop flags leaked package.json/dependency names and open registry configurations during discovery, because the attack often starts with a single leaked internal package name. It does not say "critical" without verifying the finding; we do not exaggerate. To assess your organization's software supply chain, contact DSET with our team at Ankara Hacettepe Teknokent.
Frequently asked questions
Our internal packages are already in a private registry, is there still a risk? Yes, if the package manager queries the internal registry and the public registry together. Being in a private registry is not enough; you need to configure the package manager to pull internal names ONLY from the private registry, use a scope, and reserve the internal names on public. Otherwise a higher version fake package uploaded to public still wins.
I use a lockfile, does that protect me? A lockfile is an important layer but not enough alone. The lock file pins the expected version and hash, that is, it freezes the resolution for existing dependencies. But confusion can still arise when a new dependency is added or the lockfile is regenerated. Use the lockfile together with scope, registry configuration and name reservation.
I am a small project, would I be a target? Dependency confusion is automated and name based; the attacker uploads internal package names to public in bulk, regardless of size, and waits. Every team that uses internal packages is a target. The good news is that protection is cheap and permanent: once the right registry configuration and scope are set up, they protect continuously.
Sources
- Alex Birsan, "Dependency Confusion" (2021) original research.
- OWASP, software supply chain security and dependency risks.
- Official npm, PyPI and Maven documentation: scope, index and repository configuration.
- DSET blog: CI/CD pipeline DevSecOps penetration testing and SAST, DAST, IAST and SCA selection matrix.
Kimliğinizi doğrulayın
Yetkilendirilmiş erişim alanı. Tüm giriş denemeleri kayıt altına alınır.