Respect URL-encoded credentials in redirect location (#14315)

uv currently ignores URL-encoded credentials in a redirect location.
This PR adds a check for these credentials to the redirect handling
logic. If found, they are moved to the Authorization header in the
redirect request.

Closes #11097
This commit is contained in:
John Mumm 2025-06-27 16:41:14 +02:00 committed by GitHub
parent 56266447e2
commit a824468c8b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 72 additions and 2 deletions

View file

@ -25,6 +25,7 @@ use tracing::{debug, trace};
use url::ParseError;
use url::Url;
use uv_auth::Credentials;
use uv_auth::{AuthMiddleware, Indexes};
use uv_configuration::{KeyringProviderType, TrustedHost};
use uv_fs::Simplified;
@ -725,6 +726,16 @@ fn request_into_redirect(
}
}
// Check if there are credentials on the redirect location itself.
// If so, move them to Authorization header.
if !redirect_url.username().is_empty() {
if let Some(credentials) = Credentials::from_url(&redirect_url) {
let _ = redirect_url.set_username("");
let _ = redirect_url.set_password(None);
headers.insert(AUTHORIZATION, credentials.to_header_value());
}
}
std::mem::swap(req.headers_mut(), &mut headers);
*req.url_mut() = Url::from(redirect_url);
debug!(

View file

@ -12018,6 +12018,61 @@ async fn add_redirect_cross_origin() -> Result<()> {
Ok(())
}
/// If uv receives a 302 redirect to a cross-origin server with credentials
/// in the location, use those credentials for the redirect request.
#[tokio::test]
async fn add_redirect_cross_origin_credentials_in_location() -> Result<()> {
let context = TestContext::new("3.12");
let filters = context
.filters()
.into_iter()
.chain([(r"127\.0\.0\.1:\d*", "[LOCALHOST]")])
.collect::<Vec<_>>();
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! { r#"
[project]
name = "foo"
version = "1.0.0"
requires-python = ">=3.12"
dependencies = []
"#
})?;
let redirect_server = MockServer::start().await;
Mock::given(method("GET"))
.respond_with(|req: &wiremock::Request| {
// Responds with credentials in the location
let redirect_url = redirect_url_to_base(
req,
"https://public:heron@pypi-proxy.fly.dev/basic-auth/simple/",
);
ResponseTemplate::new(302).insert_header("Location", &redirect_url)
})
.mount(&redirect_server)
.await;
let redirect_url = Url::parse(&redirect_server.uri())?;
uv_snapshot!(filters, context.add().arg("--default-index").arg(redirect_url.as_str()).arg("anyio"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 4 packages in [TIME]
Prepared 3 packages in [TIME]
Installed 3 packages in [TIME]
+ anyio==4.3.0
+ idna==3.6
+ sniffio==1.3.1
"
);
Ok(())
}
/// uv currently fails to look up keyring credentials on a cross-origin redirect.
#[tokio::test]
async fn add_redirect_with_keyring_cross_origin() -> Result<()> {
@ -12145,14 +12200,18 @@ async fn pip_install_redirect_with_netrc_cross_origin() -> Result<()> {
}
fn redirect_url_to_pypi_proxy(req: &wiremock::Request) -> String {
redirect_url_to_base(req, "https://pypi-proxy.fly.dev/basic-auth/simple/")
}
fn redirect_url_to_base(req: &wiremock::Request, base: &str) -> String {
let last_path_segment = req
.url
.path_segments()
.expect("path has segments")
.filter(|segment| !segment.is_empty()) // Filter out empty segments
.filter(|segment| !segment.is_empty())
.next_back()
.expect("path has a package segment");
format!("https://pypi-proxy.fly.dev/basic-auth/simple/{last_path_segment}/")
format!("{base}{last_path_segment}/")
}
/// Test the error message when adding a package with multiple existing references in