astral-sh / uv

An extremely fast Python package and project manager, written in Rust.
https://docs.astral.sh/uv
Apache License 2.0
28.21k stars 808 forks source link

uv suffers dependency confusion if user forgets to put username:password on internal index url #9429

Open dhandy2013 opened 6 days ago

dhandy2013 commented 6 days ago

I accidentally triggered a dependency confusion vulnerability in uv just by forgetting to add credentials to our company's internal package index URL.

Steps to reproduce:

$ uv venv scratchenv
Using CPython 3.11.10
Creating virtual environment at: scratchenv
Activate with: source scratchenv/bin/activate
$ uv pip install --python=scratchenv --index=https://internal.example.com/repository/production/simple example-component
Using Python 3.11.10 environment at scratchenv
Resolved 1 package in 870ms
Installed 1 package in 1ms
 + example-component==0.0.1
$ uv pip list --python=scratchenv
Using Python 3.11.10 environment at scratchenv
Package           Version
----------------- -------
example-component 0.0.1

Expected result:

Either example-component is installed from my internal Python package index, or else I get some kind of error message if something was wrong with the package index URL.

Actual result:

As you can see from the command output above, uv silently and happily ignored my internal package index URL, gave no warning, and installs example-component from pypi! The only reason I knew something was wrong is that uv pip list reported an unexpected version number.

Something was very wrong with my internal package index URL: It was missing the required credentials. So uv just silently ignored the error and went on to the default index URL, causing dependency confusion.

(Fortunately the example-component from pypi.org does not seem to be any kind of malware.)

It seems from the uv docs about Packages that exist on multiple indexes that uv prides itself on being better than pip at avoiding dependency confusion. So I thought you would want to hear about this case where uv is easily vulnerable to it.

Operating system: Ubuntu 24.04

$ uv version
uv 0.5.4
charliermarsh commented 6 days ago

Thanks. I'm not certain what we should change here if anything. Are you for --default-index= rather than --index=? --index= allows fallback to PyPI, if a package isn't found on a preceding index (in other words, it prevents dependency confusion attacks by disallowing packages to shadow packages on other indexes).

I don't know if there's actually anything we can do about erroring if we fail to connect to your index, because indexes in general are so bad and so inconsistent in how they handle these cases. Some indexes return 403 when you provide valid credentials but request a non-existent package; some indexes return 404 when you provide invalid credentials. No matter what we do, we'll break something. We do show a dedicated hint if we fail to resolve and see (e.g.) a 403. I guess we could consider showing that warning even if the installation completes.

charliermarsh commented 6 days ago

We should probably at least warn when we fail to connect to an index. We may want to special-case the indexes that we know return 403 consistently.

dhandy2013 commented 6 days ago

A warning would be helpful. Something in the docs to warn users would also be helpful.

Speaking of the docs, I really hate putting credentials in the package index URL. I would much rather put them in environment variables. I saw the docs for UVINDEX{name}_PASSWORD and was wondering how on earth to set the name of an index given on the command line, given that configuring the index in a pyproject.toml file wasn't going to work for me.

So I searched the source code (Thanks for writing uv in Rust!) and found in crates/uv-distribution-types/src/index.rs the implementation for Index::from_str that apparently lets you put name= in front of an index URL on the command line! I'm going to try this out right now. If this works, then I humbly request that you please document this feature!

charliermarsh commented 6 days ago

I documented it here recently: https://docs.astral.sh/uv/configuration/indexes/#defining-an-index. Take a look at "When providing an index on the command line..."

dhandy2013 commented 6 days ago

Thank you. The docs are good, I don't know why I missed that. :)

chrisrodrigue commented 5 days ago

What happens when a user configures UV_DEFAULT_INDEX to a PyPI mirror URL that does not contain credentials?

UV_INDEX_INTERNAL_PROXY_USERNAME and UV_INDEX_INTERNAL_PROXY_PASSWORD do not appear to work with the default index.

Should the variables UV_INDEX_DEFAULT_USERNAME and UV_INDEX_DEFAULT_PASSWORD apply to the default index? Or should they be UV_DEFAULT_INDEX_USERNAME and UV_DEFAULT_INDEX_PASSWORD?

dhandy2013 commented 5 days ago

Answering this previous question from @charliermarsh :

Are you for --default-index= rather than --index=? --index= allows fallback to PyPI, if a package isn't found on a preceding index

In my case I wanted example-component to come from my internal package index, but its dependencies to come from PyPI. That's why I used --index rather than --default-index to point to the internal package index.