DEV Community

Paul Brabban
Paul Brabban

Posted on • Originally published at tempered.works on

How to get pwned with --extra-index-url

A diagram illustrating a dependency confusion attack using Python's pip. The layout compares a

Python's built-in pip package manager is unsafe when used with the --extra-index-url flag (there are other dangerous variants too). An attacker can publish a malicious package with the same name and a higher version to PyPI, and their package will be installed.

This post confirms that the vulnerability (CVE-2018-20225) is still a problem today. Despite the CVSS 7.8 (High) CVSS score, the maintainers have refused to change the behaviour.

I also introduce a test suite and publicly-available test packages that you can use to more easily confirm the safety - or not - of your own setup.

Two variants of package example-package-cve-2018-20225

I've written two variants of a new package that I'll use to demonstrate the problem. The package is essentially a single __init__.py file that prints a message to show which package has been installed when it's imported, along with minimal metadata required to publish the package to a registry.

The "safe" variant

The "safe" variant of the package is at version 0.0.1. It prints this is the safe, private package when imported.

This package stands in for your intended, usually private, package. I've published it to GitLab and made the registry public for the convenience of testing.

The "malicious" variant

The "malicious" variant of the package is at version 1.0.0. There's nothing special about 1.0.0, it's just "higher" than 0.0.1.

This package prints oops, this is the malicious package when imported. It's published to PyPI.

Testing approach

I've created a GitHub actions workflow to test a variety of install and update scenarios. There are far too many potential tools and combinations to test them all, which is why I've made these packages available publicly. You can use them to test whatever specific scenario you want.

!!! warning
The usual disclaimers apply. My intentions are good, but that could change or I could be compromised in the future. Take whatever precautions you can to establish trustworthiness - I've kept the packages simple to aid manual audit.

All the tests are run against the latest versions (at time of writing) of the package management software. The tests report failure if the malicious package is installed. You can see the current latest test run in the repo's GitHub actions tab. You can also see the packages and how I published them to PyPI and GitLab in the repo.

Test scenarios

I'm trying out a few scenarios I'm interested in. What happens when you specify various combinations of flags (including forgetting the flags) with pip?

pip with and without flags

  • pip install ${PACKAGE}: 🚨 Malicious (Default behaviour if the flags are forgotten)
  • pip install ${PACKAGE} --index-url ${GITLAB_INDEX_URL}: βœ… Safe (Replaces PyPI with GitLab as the only source)
  • pip install ${PACKAGE} --extra-index-url ${GITLAB_INDEX_URL}: 🚨 Malicious (Searches both PyPI and GitLab, installs highest version)
  • pip install ${PACKAGE} --index-url ${PYPI_INDEX_URL} --extra-index-url ${GITLAB_INDEX_URL}: 🚨 Malicious (Sets PyPI as primary, GitLab as extra, same behaviour when flag order reversed)
  • export PIP_EXTRA_INDEX_URL=${GITLAB_INDEX_URL}; pip install ...: 🚨 Malicious (Uses environment variable instead of CLI flag)
  • pip install -r requirements.txt (File contains ${PACKAGE}): 🚨 Malicious (Installs from PyPI)
  • pip install ${PACKAGE} --index-url ...; pip install -U ${PACKAGE} --extra-index-url: 🚨 Malicious (Installs "safe", then runs update with both indexes to get "malicious")

GitLab's PyPI pass-through behaviour

A GitLab registry will pass through requests for packages that it doesn't hold to PyPI. This is flagged as a security risk. If you're exposed to this vulnerability, it seems like a solid step forward to me. It resolves the dependency confusion problem, works with different package managers and is easy for users.

  • pip install ${PACKAGE} requests --index-url ${GITLAB_INDEX_URL}: βœ… Safe (Installs target package + public lib (requests) from GitLab index only, succeeds and installs the right package)

Is uv vulnerable?

There are many Python package managers. uv is the current darling of the community and has different behaviour when this flag is used in a pip-like manner. I've added a couple of tests to confirm the behaviour.

  • uv pip install ${PACKAGE} --extra-index-url ${GITLAB_INDEX_URL}: βœ… Safe (Uses uv with the risky flag)
  • uv pip install ${PACKAGE} --index-strategy unsafe-best-match: 🚨 Malicious (Uses uv but forces legacy pip behaviour)

What about lockfiles?

Assuming you didn't already lock the malicious package, lockfiles only offer temporary protection. When you update, if you update unsafely, you get the malicious package.

Summary

  • There are many ways to put yourself at risk of CVE-2018-20225; if you get it wrong, an attacker has a trivially easy route onto your computer or infrastructure.
  • Being confident that what you're doing is safe isn't trivial; I've provided source code, a suite of scenario results and a test harness to help you.

Avoid

  • Using --extra-index-url with pip 🚨
  • Using --index-strategy with uv 🚨

Consider

  • Using a private registry with a PyPI pass-through as the only index, which I demonstrated with GitLab.

I doubt I would have known about this problem had it not been for a vulnerability scanner alerting me to it last year. If you're currently exposed to this problem, you're certainly not alone. When I asked ChatGPT how to safely use a private package registry, the response it generated (based, of course on the content it's been trained on) included using --extra-index-url with no mention of this risk.

ChatGPT conversation showing a response recommending pip install with --extra-index-url flag for private package registry, with text explaining this allows searching both PyPI and private registry


ChatGPT recommending the vulnerable --extra-index-url approach

Top comments (0)