Long before "dependency confusion" became a headline term in software supply chain security, a quiet GitHub issue against the Python package installer pip had already described the exact mechanism attackers would later weaponize at scale. That issue became CVE-2018-20225, and it remains one of the clearest case studies of how a reasonable-sounding default — resolve to the highest available version — can turn a private package name into a public attack surface.
CVE-2018-20225: The Vulnerability at pip's Core
CVE-2018-20225 describes a flaw in how pip resolves package versions when a project is configured to install from more than one index — typically a private, internal package repository alongside the default public PyPI. When a developer or CI pipeline points pip at both sources (commonly via the --extra-index-url flag alongside the default --index-url), pip does not treat the private index as authoritative for internal package names. Instead, it queries every configured index, compares whatever version numbers it finds, and installs the single highest version — regardless of which index it came from.
That behavior is the root cause of what the industry now calls dependency confusion: if an internal package is named, say, acme-internal-utils and is versioned 1.2.0 on a company's private index, an attacker who registers a public PyPI project with the same name and a higher version number (9.9.9, for example) can cause pip to fetch and install the attacker's package instead — even though the developer only ever intended to consume the private one. Because pip executes arbitrary code from setup.py during installation, the result is remote code execution on the developer workstation or, worse, inside a CI/CD build environment with access to secrets, source code, and deployment credentials.
Affected Versions and Components
The CVE record does not point to a single patched version range because this is not a conventional code defect fixed by a version bump — it is a design characteristic of pip's multi-index resolution logic, and the official description characterizes it as affecting pip broadly rather than one narrow release. The precondition is configuration, not a specific pip build: any environment using --extra-index-url (or an equivalent multi-index setup in a pip.conf, requirements.txt, or CI configuration) to combine a private package index with the public PyPI index is exposed, provided an attacker can predict or discover the internal package's name.
This is an important nuance for triage. Package index priority — the assumption that a named private index takes precedence over the public one — simply does not exist as a security boundary in pip's default resolver behavior. Any organization mirroring internal Python packages this way inherits the exposure, independent of which pip version is pinned in their toolchain.
CVSS, EPSS, and KEV Context
CVE-2018-20225 sits in an unusual place in vulnerability scoring history. The pip maintainers publicly disputed the classification, arguing that the behavior is documented, intentional, and not something pip can "fix" unilaterally without breaking legitimate multi-index workflows that some organizations rely on. Largely as a result of that dispute, NVD has not published a definitive CVSS base score for this CVE — it's a case where the severity conversation happened in GitHub issue threads and blog posts rather than a scored advisory. Organizations doing risk triage should not expect a clean CVSS number to drop this into an automated priority bucket; it needs to be evaluated as an architectural risk in your dependency resolution configuration, not a single patchable bug.
The same applies to EPSS and CISA's Known Exploited Vulnerabilities (KEV) catalog: CVE-2018-20225 is not listed in KEV, and EPSS scoring for CVE identifiers tied to design-level ecosystem weaknesses tends to understate real-world risk, since EPSS models exploitation of the CVE as a discrete software flaw rather than exploitation of the underlying technique across an entire class of misconfigured registries. That gap matters, because the technique this CVE documents has been used repeatedly and publicly in the years since — just not always logged against this specific identifier.
Timeline: From a GitHub Issue to a Household Name
The underlying pip behavior was raised by researchers and pip contributors well before it carried mainstream attention, and CVE-2018-20225 was reserved to formally track the issue as a security concern with pip's default index-priority handling. The pip project's response was consistent from early on: this is expected behavior tied to how multiple indexes are intentionally allowed to coexist, and the responsibility for avoiding collisions falls to the user's configuration choices rather than a code patch.
The moment that changed the industry's understanding of this class of bug came in February 2021, when security researcher Alex Birsan published "Dependency Confusion," detailing how he used the exact same version-priority weakness — across pip, npm, and other package managers — to get proof-of-concept packages executed inside the internal build systems of companies including Apple, Microsoft, PayPal, Netflix, and Uber. That research turned a years-old, disputed CVE into one of the most cited supply chain attack patterns in the industry, and it pushed package registries and enterprise tooling vendors to ship mitigations: PyPI and npm both introduced mechanisms to help organizations reserve or claim internal package namespaces, and private registry products (Artifactory, Azure Artifacts, AWS CodeArtifact, and others) added "upstream source" controls that can pin a package name to a single trusted origin rather than resolving across indexes by version number alone.
Remediation and Mitigation Steps
Because CVE-2018-20225 is a configuration-driven exposure rather than a patchable defect, remediation is about closing the resolution ambiguity rather than upgrading pip:
- Stop combining indexes by priority assumption. Avoid
--extra-index-urlfor any workflow where a private and public index might share a namespace. Where possible, use--index-urlpointed at a private index that itself proxies and caches the public packages you need, so pip only ever talks to one trusted source. - Enforce package origin pinning in your registry. Modern private registry products support "upstream source protection" or equivalent controls that lock a package name to a specific, designated index and refuse to silently substitute a higher-versioned package from elsewhere. Enable this for every internal package name.
- Reserve your internal package names on PyPI. Even placeholder, empty uploads under your organization's internal package names prevent an attacker from squatting that name on the public index — a direct, low-effort countermeasure to dependency confusion.
- Use hash-checking mode. Running pip with
--require-hashesand pinned hashes in your requirements file causes installation to fail loudly if the resolved package doesn't match the exact artifact you expect, catching a substituted package before it executes any code. - Pin exact versions and use lockfiles. Tools like pip-tools or Poetry that generate fully pinned lockfiles reduce the chance that a resolver silently jumps to a newer, attacker-controlled version during a routine install.
- Monitor for namespace collisions continuously. Set up alerting for any newly published PyPI package matching your internal naming conventions — attackers relying on this technique typically need only a single successful install to succeed.
How Safeguard Helps
Dependency confusion vulnerabilities like CVE-2018-20225 are exactly the class of risk that slips through traditional vulnerability scanning, because there's no CVE patch to apply and no single version to bump. Safeguard is built to close this gap at the software supply chain layer rather than the code layer.
Safeguard continuously inventories every package your builds and CI/CD pipelines pull from PyPI and other registries, generating accurate SBOMs that capture not just package names and versions but their resolved origin — flagging cases where a build resolved a dependency from an unexpected or newly registered public source instead of your intended private index. Policy enforcement can require that internal package names only ever resolve through an allowlisted, pinned index, blocking builds outright if pip's version-based resolution attempts to reach outside that boundary — directly neutralizing the mechanism behind CVE-2018-20225.
Safeguard also watches public registries for namespace collisions: if a package matching one of your internal project names appears on PyPI, you're alerted before an unsuspecting build ever has the chance to install it. Combined with provenance verification and hash validation gates in CI/CD, Safeguard turns a historically "unfixed by design" issue into something your pipeline actively defends against — closing the gap between a disputed, unscored CVE and the very real, ongoing risk it represents to modern Python supply chains.