From e9df9bed5f82ee879384732aeb285d3a60bd0dfd Mon Sep 17 00:00:00 2001 From: John Mumm Date: Fri, 20 Jun 2025 12:36:34 +0200 Subject: [PATCH 1/2] Disambiguate relative URLs passed to `--index` --- crates/uv-cli/src/lib.rs | 3 + crates/uv-distribution-types/src/index.rs | 65 +++++++++++++++++++ crates/uv-distribution-types/src/index_url.rs | 3 +- crates/uv/tests/it/edit.rs | 39 +++++++++-- docs/reference/cli.md | 18 +++++ 5 files changed, 123 insertions(+), 5 deletions(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index bd06f9a8278bf..4eef2dcbfda47 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -5068,6 +5068,9 @@ pub struct IndexArgs { /// All indexes provided via this flag take priority over the index specified by /// `--default-index` (which defaults to PyPI). When multiple `--index` flags are provided, /// earlier values take priority. + /// + /// Index names are not supported as values. Relative paths must be disambiguated from index + /// names with `./` or `../` on Unix or `.\\`, `..\\`, `./` or `../` on Windows. // // The nested Vec structure (`Vec>>`) is required for clap's // value parsing mechanism, which processes one value at a time, in order to handle diff --git a/crates/uv-distribution-types/src/index.rs b/crates/uv-distribution-types/src/index.rs index 8ac7c3cd4892d..064f915b43861 100644 --- a/crates/uv-distribution-types/src/index.rs +++ b/crates/uv-distribution-types/src/index.rs @@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; use uv_auth::{AuthPolicy, Credentials}; +use uv_pep508::{Scheme, split_scheme}; use uv_redacted::DisplaySafeUrl; use crate::index_name::{IndexName, IndexNameError}; @@ -278,6 +279,9 @@ impl FromStr for Index { } // Otherwise, assume the source is a URL. + if !is_disambiguated_path(s) { + return Err(IndexSourceError::AmbiguousPath(s.to_string())); + } let url = IndexUrl::from_str(s)?; Ok(Self { name: None, @@ -293,6 +297,27 @@ impl FromStr for Index { } } +/// Checks if a path is disambiguated. +/// +/// Disambiguated paths are absolute paths, paths with valid schemes, +/// and paths starting with "./" (or ".\\" on Windows). +fn is_disambiguated_path(path: &str) -> bool { + if cfg!(windows) { + if path.starts_with(".\\") || path.starts_with("..\\") || path.starts_with('/') { + return true; + } + } + if path.starts_with("./") || path.starts_with("../") || Path::new(path).is_absolute() { + return true; + } + // Check if the path has a scheme (like `https://`) + if let Some((scheme, _)) = split_scheme(path) { + return Scheme::parse(scheme).is_some(); + } + // This is an ambiguous relative path + false +} + /// An [`IndexUrl`] along with the metadata necessary to query the index. #[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)] pub struct IndexMetadata { @@ -383,4 +408,44 @@ pub enum IndexSourceError { IndexName(#[from] IndexNameError), #[error("Index included a name, but the name was empty")] EmptyName, + #[error("Relative paths must be disambiguated from index names (use `./{0}`)")] + AmbiguousPath(String), +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_index_url_parse_valid_paths() { + // Valid URL + assert!(is_disambiguated_path("https://pypi.org/simple")); + // Absolute path + assert!(is_disambiguated_path("/absolute/path")); + // Windows absolute path + #[cfg(windows)] + assert!(is_disambiguated_path("C:/absolute/path")); + // Relative path + assert!(is_disambiguated_path("./relative/path")); + // Windows relative path + #[cfg(windows)] + assert!(is_disambiguated_path(".\\relative\\path")); + } + + #[test] + fn test_index_url_parse_ambiguous_paths() { + // Test multi-segment ambiguous path (no `./` prefix) + assert!(!is_disambiguated_path("relative/path")); + // Test single-segment ambiguous path + assert!(!is_disambiguated_path("index")); + } + + #[test] + fn test_index_url_parse_with_schemes() { + assert!(is_disambiguated_path("file:///absolute/path")); + assert!(is_disambiguated_path("https://registry.com/simple/")); + assert!(is_disambiguated_path( + "git+https://github.com/example/repo.git" + )); + } } diff --git a/crates/uv-distribution-types/src/index_url.rs b/crates/uv-distribution-types/src/index_url.rs index a523b48114ec5..cc92c57cef84e 100644 --- a/crates/uv-distribution-types/src/index_url.rs +++ b/crates/uv-distribution-types/src/index_url.rs @@ -36,7 +36,8 @@ impl IndexUrl { /// Parse an [`IndexUrl`] from a string, relative to an optional root directory. /// /// If no root directory is provided, relative paths are resolved against the current working - /// directory. + /// directory. Relative paths must be disambiguated by starting with "./" or "../" on Unix or + /// `.\\`, `..\\`, `./` or `../` on Windows. pub fn parse(path: &str, root_dir: Option<&Path>) -> Result { let url = match split_scheme(path) { Some((scheme, ..)) => { diff --git a/crates/uv/tests/it/edit.rs b/crates/uv/tests/it/edit.rs index d117b1e8c9f5b..f2a592a379896 100644 --- a/crates/uv/tests/it/edit.rs +++ b/crates/uv/tests/it/edit.rs @@ -9364,7 +9364,7 @@ fn add_index_with_existing_relative_path_index() -> Result<()> { let wheel_dst = packages.child("ok-1.0.0-py3-none-any.whl"); fs_err::copy(&wheel_src, &wheel_dst)?; - uv_snapshot!(context.filters(), context.add().arg("iniconfig").arg("--index").arg("test-index"), @r" + uv_snapshot!(context.filters(), context.add().arg("iniconfig").arg("--index").arg("./test-index"), @r" success: true exit_code: 0 ----- stdout ----- @@ -9393,7 +9393,7 @@ fn add_index_with_non_existent_relative_path() -> Result<()> { dependencies = [] "#})?; - uv_snapshot!(context.filters(), context.add().arg("iniconfig").arg("--index").arg("test-index"), @r" + uv_snapshot!(context.filters(), context.add().arg("iniconfig").arg("--index").arg("./test-index"), @r" success: false exit_code: 2 ----- stdout ----- @@ -9423,7 +9423,7 @@ fn add_index_with_non_existent_relative_path_with_same_name_as_index() -> Result url = "https://pypi-proxy.fly.dev/simple" "#})?; - uv_snapshot!(context.filters(), context.add().arg("iniconfig").arg("--index").arg("test-index"), @r" + uv_snapshot!(context.filters(), context.add().arg("iniconfig").arg("--index").arg("./test-index"), @r" success: false exit_code: 2 ----- stdout ----- @@ -9446,12 +9446,16 @@ fn add_index_empty_directory() -> Result<()> { version = "0.1.0" requires-python = ">=3.12" dependencies = [] + + [[tool.uv.index]] + name = "test-index" + url = "https://pypi-proxy.fly.dev/simple" "#})?; let packages = context.temp_dir.child("test-index"); packages.create_dir_all()?; - uv_snapshot!(context.filters(), context.add().arg("iniconfig").arg("--index").arg("test-index"), @r" + uv_snapshot!(context.filters(), context.add().arg("iniconfig").arg("--index").arg("./test-index"), @r" success: true exit_code: 0 ----- stdout ----- @@ -9467,6 +9471,33 @@ fn add_index_empty_directory() -> Result<()> { Ok(()) } +#[test] +fn add_index_with_ambiguous_relative_path() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + "#})?; + + uv_snapshot!(context.filters(), context.add().arg("iniconfig").arg("--index").arg("test-index"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: invalid value 'test-index' for '--index ': Relative paths must be disambiguated from index names (use `./test-index`) + + For more information, try '--help'. + "); + + Ok(()) +} + /// Add a PyPI requirement. #[test] fn add_group_comment() -> Result<()> { diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 9ae05a8e00660..552826b747a94 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -123,6 +123,7 @@ uv run [OPTIONS] [COMMAND]
--index index

The URLs to use when resolving dependencies, in addition to the default index.

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.

+

Index names are not supported as values. Relative paths must be disambiguated from index names with ./ or ../ on Unix or .\\, ..\\, ./ or ../ on Windows.

May also be set with the UV_INDEX environment variable.

--index-strategy index-strategy

The strategy to use when resolving against multiple index URLs.

By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-index). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.

May also be set with the UV_INDEX_STRATEGY environment variable.

Possible values:

@@ -479,6 +480,7 @@ uv add [OPTIONS] >
--index index

The URLs to use when resolving dependencies, in addition to the default index.

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.

+

Index names are not supported as values. Relative paths must be disambiguated from index names with ./ or ../ on Unix or .\\, ..\\, ./ or ../ on Windows.

May also be set with the UV_INDEX environment variable.

--index-strategy index-strategy

The strategy to use when resolving against multiple index URLs.

By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-index). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.

May also be set with the UV_INDEX_STRATEGY environment variable.

Possible values:

@@ -663,6 +665,7 @@ uv remove [OPTIONS] ...
--index index

The URLs to use when resolving dependencies, in addition to the default index.

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.

+

Index names are not supported as values. Relative paths must be disambiguated from index names with ./ or ../ on Unix or .\\, ..\\, ./ or ../ on Windows.

May also be set with the UV_INDEX environment variable.

--index-strategy index-strategy

The strategy to use when resolving against multiple index URLs.

By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-index). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.

May also be set with the UV_INDEX_STRATEGY environment variable.

Possible values:

@@ -832,6 +835,7 @@ uv version [OPTIONS] [VALUE]
--index index

The URLs to use when resolving dependencies, in addition to the default index.

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.

+

Index names are not supported as values. Relative paths must be disambiguated from index names with ./ or ../ on Unix or .\\, ..\\, ./ or ../ on Windows.

May also be set with the UV_INDEX environment variable.

--index-strategy index-strategy

The strategy to use when resolving against multiple index URLs.

By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-index). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.

May also be set with the UV_INDEX_STRATEGY environment variable.

Possible values:

@@ -1022,6 +1026,7 @@ uv sync [OPTIONS]
--index index

The URLs to use when resolving dependencies, in addition to the default index.

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.

+

Index names are not supported as values. Relative paths must be disambiguated from index names with ./ or ../ on Unix or .\\, ..\\, ./ or ../ on Windows.

May also be set with the UV_INDEX environment variable.

--index-strategy index-strategy

The strategy to use when resolving against multiple index URLs.

By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-index). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.

May also be set with the UV_INDEX_STRATEGY environment variable.

Possible values:

@@ -1210,6 +1215,7 @@ uv lock [OPTIONS]
--index index

The URLs to use when resolving dependencies, in addition to the default index.

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.

+

Index names are not supported as values. Relative paths must be disambiguated from index names with ./ or ../ on Unix or .\\, ..\\, ./ or ../ on Windows.

May also be set with the UV_INDEX environment variable.

--index-strategy index-strategy

The strategy to use when resolving against multiple index URLs.

By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-index). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.

May also be set with the UV_INDEX_STRATEGY environment variable.

Possible values:

@@ -1383,6 +1389,7 @@ uv export [OPTIONS]
--index index

The URLs to use when resolving dependencies, in addition to the default index.

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.

+

Index names are not supported as values. Relative paths must be disambiguated from index names with ./ or ../ on Unix or .\\, ..\\, ./ or ../ on Windows.

May also be set with the UV_INDEX environment variable.

--index-strategy index-strategy

The strategy to use when resolving against multiple index URLs.

By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-index). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.

May also be set with the UV_INDEX_STRATEGY environment variable.

Possible values:

@@ -1568,6 +1575,7 @@ uv tree [OPTIONS]
--index index

The URLs to use when resolving dependencies, in addition to the default index.

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.

+

Index names are not supported as values. Relative paths must be disambiguated from index names with ./ or ../ on Unix or .\\, ..\\, ./ or ../ on Windows.

May also be set with the UV_INDEX environment variable.

--index-strategy index-strategy

The strategy to use when resolving against multiple index URLs.

By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-index). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.

May also be set with the UV_INDEX_STRATEGY environment variable.

Possible values:

@@ -1827,6 +1835,7 @@ uv tool run [OPTIONS] [COMMAND]
--index index

The URLs to use when resolving dependencies, in addition to the default index.

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.

+

Index names are not supported as values. Relative paths must be disambiguated from index names with ./ or ../ on Unix or .\\, ..\\, ./ or ../ on Windows.

May also be set with the UV_INDEX environment variable.

--index-strategy index-strategy

The strategy to use when resolving against multiple index URLs.

By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-index). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.

May also be set with the UV_INDEX_STRATEGY environment variable.

Possible values:

@@ -1997,6 +2006,7 @@ uv tool install [OPTIONS]
--index index

The URLs to use when resolving dependencies, in addition to the default index.

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.

+

Index names are not supported as values. Relative paths must be disambiguated from index names with ./ or ../ on Unix or .\\, ..\\, ./ or ../ on Windows.

May also be set with the UV_INDEX environment variable.

--index-strategy index-strategy

The strategy to use when resolving against multiple index URLs.

By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-index). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.

May also be set with the UV_INDEX_STRATEGY environment variable.

Possible values:

@@ -2157,6 +2167,7 @@ uv tool upgrade [OPTIONS] ...
--index index

The URLs to use when resolving dependencies, in addition to the default index.

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.

+

Index names are not supported as values. Relative paths must be disambiguated from index names with ./ or ../ on Unix or .\\, ..\\, ./ or ../ on Windows.

May also be set with the UV_INDEX environment variable.

--index-strategy index-strategy

The strategy to use when resolving against multiple index URLs.

By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-index). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.

May also be set with the UV_INDEX_STRATEGY environment variable.

Possible values:

@@ -3166,6 +3177,7 @@ uv pip compile [OPTIONS] >
--index index

The URLs to use when resolving dependencies, in addition to the default index.

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.

+

Index names are not supported as values. Relative paths must be disambiguated from index names with ./ or ../ on Unix or .\\, ..\\, ./ or ../ on Windows.

May also be set with the UV_INDEX environment variable.

--index-strategy index-strategy

The strategy to use when resolving against multiple index URLs.

By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-index). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.

May also be set with the UV_INDEX_STRATEGY environment variable.

Possible values:

@@ -3444,6 +3456,7 @@ uv pip sync [OPTIONS] ...
--index index

The URLs to use when resolving dependencies, in addition to the default index.

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.

+

Index names are not supported as values. Relative paths must be disambiguated from index names with ./ or ../ on Unix or .\\, ..\\, ./ or ../ on Windows.

May also be set with the UV_INDEX environment variable.

--index-strategy index-strategy

The strategy to use when resolving against multiple index URLs.

By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-index). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.

May also be set with the UV_INDEX_STRATEGY environment variable.

Possible values:

@@ -3707,6 +3720,7 @@ uv pip install [OPTIONS] |--editable
--index index

The URLs to use when resolving dependencies, in addition to the default index.

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.

+

Index names are not supported as values. Relative paths must be disambiguated from index names with ./ or ../ on Unix or .\\, ..\\, ./ or ../ on Windows.

May also be set with the UV_INDEX environment variable.

--index-strategy index-strategy

The strategy to use when resolving against multiple index URLs.

By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-index). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.

May also be set with the UV_INDEX_STRATEGY environment variable.

Possible values:

@@ -4125,6 +4139,7 @@ uv pip list [OPTIONS]
--index index

The URLs to use when resolving dependencies, in addition to the default index.

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.

+

Index names are not supported as values. Relative paths must be disambiguated from index names with ./ or ../ on Unix or .\\, ..\\, ./ or ../ on Windows.

May also be set with the UV_INDEX environment variable.

--index-strategy index-strategy

The strategy to use when resolving against multiple index URLs.

By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-index). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.

May also be set with the UV_INDEX_STRATEGY environment variable.

Possible values:

@@ -4298,6 +4313,7 @@ uv pip tree [OPTIONS]
--index index

The URLs to use when resolving dependencies, in addition to the default index.

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.

+

Index names are not supported as values. Relative paths must be disambiguated from index names with ./ or ../ on Unix or .\\, ..\\, ./ or ../ on Windows.

May also be set with the UV_INDEX environment variable.

--index-strategy index-strategy

The strategy to use when resolving against multiple index URLs.

By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-index). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.

May also be set with the UV_INDEX_STRATEGY environment variable.

Possible values:

@@ -4485,6 +4501,7 @@ uv venv [OPTIONS] [PATH]
--index index

The URLs to use when resolving dependencies, in addition to the default index.

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.

+

Index names are not supported as values. Relative paths must be disambiguated from index names with ./ or ../ on Unix or .\\, ..\\, ./ or ../ on Windows.

May also be set with the UV_INDEX environment variable.

--index-strategy index-strategy

The strategy to use when resolving against multiple index URLs.

By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-index). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.

May also be set with the UV_INDEX_STRATEGY environment variable.

Possible values:

@@ -4635,6 +4652,7 @@ uv build [OPTIONS] [SRC]
--index index

The URLs to use when resolving dependencies, in addition to the default index.

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.

+

Index names are not supported as values. Relative paths must be disambiguated from index names with ./ or ../ on Unix or .\\, ..\\, ./ or ../ on Windows.

May also be set with the UV_INDEX environment variable.

--index-strategy index-strategy

The strategy to use when resolving against multiple index URLs.

By default, uv will stop at the first index on which a given package is available, and limit resolutions to those present on that first index (first-index). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.

May also be set with the UV_INDEX_STRATEGY environment variable.

Possible values:

From af2b605197ae39da70f39e3f50bc733f6ff62238 Mon Sep 17 00:00:00 2001 From: John Mumm Date: Mon, 23 Jun 2025 14:00:01 +0200 Subject: [PATCH 2/2] Use warning --- Cargo.lock | 1 + crates/uv-distribution-types/Cargo.toml | 1 + crates/uv-distribution-types/src/index.rs | 65 -------------- crates/uv-distribution-types/src/index_url.rs | 88 ++++++++++++++++++- crates/uv/src/settings.rs | 6 ++ crates/uv/tests/it/edit.rs | 19 +++- 6 files changed, 110 insertions(+), 70 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a30a0cbe15432..095ba90c1370c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5176,6 +5176,7 @@ dependencies = [ "uv-pypi-types", "uv-redacted", "uv-small-str", + "uv-warnings", "version-ranges", ] diff --git a/crates/uv-distribution-types/Cargo.toml b/crates/uv-distribution-types/Cargo.toml index dc5a70166d91b..1ca28c5eda0cf 100644 --- a/crates/uv-distribution-types/Cargo.toml +++ b/crates/uv-distribution-types/Cargo.toml @@ -29,6 +29,7 @@ uv-platform-tags = { workspace = true } uv-pypi-types = { workspace = true } uv-redacted = { workspace = true } uv-small-str = { workspace = true } +uv-warnings = { workspace = true } arcstr = { workspace = true } bitflags = { workspace = true } diff --git a/crates/uv-distribution-types/src/index.rs b/crates/uv-distribution-types/src/index.rs index 064f915b43861..8ac7c3cd4892d 100644 --- a/crates/uv-distribution-types/src/index.rs +++ b/crates/uv-distribution-types/src/index.rs @@ -5,7 +5,6 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; use uv_auth::{AuthPolicy, Credentials}; -use uv_pep508::{Scheme, split_scheme}; use uv_redacted::DisplaySafeUrl; use crate::index_name::{IndexName, IndexNameError}; @@ -279,9 +278,6 @@ impl FromStr for Index { } // Otherwise, assume the source is a URL. - if !is_disambiguated_path(s) { - return Err(IndexSourceError::AmbiguousPath(s.to_string())); - } let url = IndexUrl::from_str(s)?; Ok(Self { name: None, @@ -297,27 +293,6 @@ impl FromStr for Index { } } -/// Checks if a path is disambiguated. -/// -/// Disambiguated paths are absolute paths, paths with valid schemes, -/// and paths starting with "./" (or ".\\" on Windows). -fn is_disambiguated_path(path: &str) -> bool { - if cfg!(windows) { - if path.starts_with(".\\") || path.starts_with("..\\") || path.starts_with('/') { - return true; - } - } - if path.starts_with("./") || path.starts_with("../") || Path::new(path).is_absolute() { - return true; - } - // Check if the path has a scheme (like `https://`) - if let Some((scheme, _)) = split_scheme(path) { - return Scheme::parse(scheme).is_some(); - } - // This is an ambiguous relative path - false -} - /// An [`IndexUrl`] along with the metadata necessary to query the index. #[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)] pub struct IndexMetadata { @@ -408,44 +383,4 @@ pub enum IndexSourceError { IndexName(#[from] IndexNameError), #[error("Index included a name, but the name was empty")] EmptyName, - #[error("Relative paths must be disambiguated from index names (use `./{0}`)")] - AmbiguousPath(String), -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_index_url_parse_valid_paths() { - // Valid URL - assert!(is_disambiguated_path("https://pypi.org/simple")); - // Absolute path - assert!(is_disambiguated_path("/absolute/path")); - // Windows absolute path - #[cfg(windows)] - assert!(is_disambiguated_path("C:/absolute/path")); - // Relative path - assert!(is_disambiguated_path("./relative/path")); - // Windows relative path - #[cfg(windows)] - assert!(is_disambiguated_path(".\\relative\\path")); - } - - #[test] - fn test_index_url_parse_ambiguous_paths() { - // Test multi-segment ambiguous path (no `./` prefix) - assert!(!is_disambiguated_path("relative/path")); - // Test single-segment ambiguous path - assert!(!is_disambiguated_path("index")); - } - - #[test] - fn test_index_url_parse_with_schemes() { - assert!(is_disambiguated_path("file:///absolute/path")); - assert!(is_disambiguated_path("https://registry.com/simple/")); - assert!(is_disambiguated_path( - "git+https://github.com/example/repo.git" - )); - } } diff --git a/crates/uv-distribution-types/src/index_url.rs b/crates/uv-distribution-types/src/index_url.rs index cc92c57cef84e..085082f372d9b 100644 --- a/crates/uv-distribution-types/src/index_url.rs +++ b/crates/uv-distribution-types/src/index_url.rs @@ -12,6 +12,7 @@ use url::{ParseError, Url}; use uv_pep508::{Scheme, VerbatimUrl, VerbatimUrlError, split_scheme}; use uv_redacted::DisplaySafeUrl; +use uv_warnings::warn_user; use crate::{Index, IndexStatusCodeStrategy, Verbatim}; @@ -36,8 +37,7 @@ impl IndexUrl { /// Parse an [`IndexUrl`] from a string, relative to an optional root directory. /// /// If no root directory is provided, relative paths are resolved against the current working - /// directory. Relative paths must be disambiguated by starting with "./" or "../" on Unix or - /// `.\\`, `..\\`, `./` or `../` on Windows. + /// directory. pub fn parse(path: &str, root_dir: Option<&Path>) -> Result { let url = match split_scheme(path) { Some((scheme, ..)) => { @@ -141,6 +141,30 @@ impl IndexUrl { Cow::Owned(url) } } + + /// Warn user if the given URL was provided as an ambiguous relative path. + /// + /// This is a temporary warning. Ambiguous values will not be + /// accepted in the future. + pub fn warn_on_disambiguated_relative_path(&self) { + let Self::Path(verbatim_url) = &self else { + return; + }; + + if let Some(path) = verbatim_url.given() { + if !is_disambiguated_path(path) { + if cfg!(windows) { + warn_user!( + "Relative paths passed to `--index` or `--default-index` should be disambiguated from index names (use `.\\{path}` or `./{path}`). Support for ambiguous values will be removed in the future" + ); + } else { + warn_user!( + "Relative paths passed to `--index` or `--default-index` should be disambiguated from index names (use `./{path}`). Support for ambiguous values will be removed in the future" + ); + } + } + } + } } impl Display for IndexUrl { @@ -163,6 +187,28 @@ impl Verbatim for IndexUrl { } } +/// Checks if a path is disambiguated. +/// +/// Disambiguated paths are absolute paths, paths with valid schemes, +/// and paths starting with "./" or "../" on Unix or ".\\", "..\\", +/// "./", or "../" on Windows. +fn is_disambiguated_path(path: &str) -> bool { + if cfg!(windows) { + if path.starts_with(".\\") || path.starts_with("..\\") || path.starts_with('/') { + return true; + } + } + if path.starts_with("./") || path.starts_with("../") || Path::new(path).is_absolute() { + return true; + } + // Check if the path has a scheme (like `file://`) + if let Some((scheme, _)) = split_scheme(path) { + return Scheme::parse(scheme).is_some(); + } + // This is an ambiguous relative path + false +} + /// An error that can occur when parsing an [`IndexUrl`]. #[derive(Error, Debug)] pub enum IndexUrlError { @@ -626,3 +672,41 @@ impl IndexCapabilities { .insert(Flags::FORBIDDEN); } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_index_url_parse_valid_paths() { + // Absolute path + assert!(is_disambiguated_path("/absolute/path")); + // Relative path + assert!(is_disambiguated_path("./relative/path")); + assert!(is_disambiguated_path("../../relative/path")); + if cfg!(windows) { + // Windows absolute path + assert!(is_disambiguated_path("C:/absolute/path")); + // Windows relative path + assert!(is_disambiguated_path(".\\relative\\path")); + assert!(is_disambiguated_path("..\\..\\relative\\path")); + } + } + + #[test] + fn test_index_url_parse_ambiguous_paths() { + // Test single-segment ambiguous path + assert!(!is_disambiguated_path("index")); + // Test multi-segment ambiguous path + assert!(!is_disambiguated_path("relative/path")); + } + + #[test] + fn test_index_url_parse_with_schemes() { + assert!(is_disambiguated_path("file:///absolute/path")); + assert!(is_disambiguated_path("https://registry.com/simple/")); + assert!(is_disambiguated_path( + "git+https://github.com/example/repo.git" + )); + } +} diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index fb1a62b416114..4c9deee64b7cd 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -1333,6 +1333,12 @@ impl AddSettings { ) .collect::>(); + // Warn user if an ambiguous relative path was passed as a value for + // `--index` or `--default-index`. + indexes + .iter() + .for_each(|index| index.url().warn_on_disambiguated_relative_path()); + // If the user passed an `--index-url` or `--extra-index-url`, warn. if installer .index_args diff --git a/crates/uv/tests/it/edit.rs b/crates/uv/tests/it/edit.rs index f2a592a379896..59547d34b97b8 100644 --- a/crates/uv/tests/it/edit.rs +++ b/crates/uv/tests/it/edit.rs @@ -9474,6 +9474,8 @@ fn add_index_empty_directory() -> Result<()> { #[test] fn add_index_with_ambiguous_relative_path() -> Result<()> { let context = TestContext::new("3.12"); + let mut filters = context.filters(); + filters.push((r"\./|\.\\\\", r"[PREFIX]")); let pyproject_toml = context.temp_dir.child("pyproject.toml"); pyproject_toml.write_str(indoc! {r#" @@ -9484,15 +9486,26 @@ fn add_index_with_ambiguous_relative_path() -> Result<()> { dependencies = [] "#})?; - uv_snapshot!(context.filters(), context.add().arg("iniconfig").arg("--index").arg("test-index"), @r" + #[cfg(unix)] + uv_snapshot!(filters, context.add().arg("iniconfig").arg("--index").arg("test-index"), @r" success: false exit_code: 2 ----- stdout ----- ----- stderr ----- - error: invalid value 'test-index' for '--index ': Relative paths must be disambiguated from index names (use `./test-index`) + warning: Relative paths passed to `--index` or `--default-index` should be disambiguated from index names (use `[PREFIX]test-index`). Support for ambiguous values will be removed in the future + error: Directory not found for index: file://[TEMP_DIR]/test-index + "); - For more information, try '--help'. + #[cfg(windows)] + uv_snapshot!(filters, context.add().arg("iniconfig").arg("--index").arg("test-index"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + warning: Relative paths passed to `--index` or `--default-index` should be disambiguated from index names (use `[PREFIX]test-index` or `[PREFIX]test-index`). Support for ambiguous values will be removed in the future + error: Directory not found for index: file://[TEMP_DIR]/test-index "); Ok(())