Compare commits
13 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0d87e4172 | ||
|
|
ac655bc5de | ||
|
|
08dc5bf450 | ||
|
|
c05c4a66bb | ||
|
|
47955c1bf6 | ||
|
|
9d73fb50fc | ||
|
|
415ed181e5 | ||
|
|
3f4a4319ae | ||
|
|
9583373c97 | ||
|
|
c8ed4920c4 | ||
|
|
5fd8a26f31 | ||
|
|
a5bfb663f2 | ||
|
|
3f3ab3fbad |
9 changed files with 534 additions and 92 deletions
38
09a03f6cfa.patch
Normal file
38
09a03f6cfa.patch
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
From 09a03f6cfaeecae8bf5774ae371d37c4369e2da4 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Damian Shaw <damian.peter.shaw@gmail.com>
|
||||||
|
Date: Sat, 15 Aug 2026 13:36:05 -0400
|
||||||
|
Subject: [PATCH] Allow flit-core 4 to build pip
|
||||||
|
|
||||||
|
---
|
||||||
|
pyproject.toml | 6 +++---
|
||||||
|
1 file changed, 3 insertions(+), 3 deletions(-)
|
||||||
|
|
||||||
|
diff --git a/pyproject.toml b/pyproject.toml
|
||||||
|
index 48f64c8018..fe6b39913d 100644
|
||||||
|
--- a/pyproject.toml
|
||||||
|
+++ b/pyproject.toml
|
||||||
|
@@ -45,13 +45,13 @@ Source = "https://github.com/pypa/pip"
|
||||||
|
Changelog = "https://pip.pypa.io/en/stable/news/"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
-requires = ["flit-core >=3.11,<4"]
|
||||||
|
+requires = ["flit-core >=3.11,<5"]
|
||||||
|
build-backend = "flit_core.buildapi"
|
||||||
|
|
||||||
|
[dependency-groups]
|
||||||
|
test = [
|
||||||
|
"cryptography",
|
||||||
|
- "flit-core >= 3.11, < 4",
|
||||||
|
+ "flit-core >= 3.11, < 5",
|
||||||
|
"freezegun",
|
||||||
|
"installer",
|
||||||
|
# pytest-subket requires 7.0+
|
||||||
|
@@ -69,7 +69,7 @@ test = [
|
||||||
|
]
|
||||||
|
|
||||||
|
test-common-wheels = [
|
||||||
|
- "flit-core >= 3.11, < 4",
|
||||||
|
+ "flit-core >= 3.11, < 5",
|
||||||
|
# We pin setuptools<80 because our test suite currently
|
||||||
|
# depends on setup.py develop to generate egg-link files.
|
||||||
|
"setuptools >= 70.1.0, <80",
|
||||||
347
10dfb6b900.patch
Normal file
347
10dfb6b900.patch
Normal file
|
|
@ -0,0 +1,347 @@
|
||||||
|
From 10dfb6b9005484578b386f64b9f36982e3dc6679 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Damian Shaw <damian.peter.shaw@gmail.com>
|
||||||
|
Date: Tue, 30 Jun 2026 21:52:39 -0400
|
||||||
|
Subject: [PATCH] Fix Link.filename decoding URL path twice (#14110)
|
||||||
|
|
||||||
|
Link already percent-decodes the URL path into `self._path`, but
|
||||||
|
`Link.filename` decoded the basename again, so a doubly-encoded
|
||||||
|
separator was decoded twice: `%252F` became `%2F` in `__init__`, then
|
||||||
|
`/` in `filename`, turning the single component `a%2Fb.whl` into
|
||||||
|
`a/b.whl`.
|
||||||
|
|
||||||
|
Drop the second decode, and add a `join_within_directory` helper so the
|
||||||
|
download-path joins treat the name as a single path component.
|
||||||
|
---
|
||||||
|
news/14110.bugfix.rst | 1 +
|
||||||
|
src/pip/_internal/models/link.py | 63 ++++++++++---
|
||||||
|
src/pip/_internal/network/download.py | 20 +++--
|
||||||
|
src/pip/_internal/operations/prepare.py | 6 +-
|
||||||
|
tests/unit/test_link.py | 113 +++++++++++++++++++++++-
|
||||||
|
5 files changed, 182 insertions(+), 21 deletions(-)
|
||||||
|
create mode 100644 news/14110.bugfix.rst
|
||||||
|
|
||||||
|
diff --git a/news/14110.bugfix.rst b/news/14110.bugfix.rst
|
||||||
|
new file mode 100644
|
||||||
|
index 0000000000..f7d4f78882
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/news/14110.bugfix.rst
|
||||||
|
@@ -0,0 +1 @@
|
||||||
|
+Fix ``Link.filename`` decoding the URL path twice.
|
||||||
|
diff --git a/src/pip/_internal/models/link.py b/src/pip/_internal/models/link.py
|
||||||
|
index 0a09c66222..cbbe945c17 100644
|
||||||
|
--- a/src/pip/_internal/models/link.py
|
||||||
|
+++ b/src/pip/_internal/models/link.py
|
||||||
|
@@ -13,6 +13,7 @@
|
||||||
|
from typing import (
|
||||||
|
Any,
|
||||||
|
NamedTuple,
|
||||||
|
+ NewType,
|
||||||
|
)
|
||||||
|
|
||||||
|
from pip._internal.exceptions import InvalidEggFragment
|
||||||
|
@@ -30,6 +31,49 @@
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
+# A single path component: percent-decoded once and reduced to a basename, so it
|
||||||
|
+# contains no path separator and is not a ``.`` or ``..`` reference. The empty
|
||||||
|
+# string means "no component".
|
||||||
|
+PathComponent = NewType("PathComponent", str)
|
||||||
|
+
|
||||||
|
+
|
||||||
|
+def _to_path_component(name: str) -> PathComponent:
|
||||||
|
+ """Reduce ``name`` to a single path component, or ``""`` if it has none.
|
||||||
|
+
|
||||||
|
+ ``os.path.basename`` drops any directory part, drive letter, or separator;
|
||||||
|
+ a ``.``, ``..``, or empty result is not a component and becomes ``""``.
|
||||||
|
+ """
|
||||||
|
+ name = os.path.basename(name)
|
||||||
|
+ if name in ("", os.curdir, os.pardir):
|
||||||
|
+ return PathComponent("")
|
||||||
|
+
|
||||||
|
+ return PathComponent(name)
|
||||||
|
+
|
||||||
|
+
|
||||||
|
+def as_path_component(name: str) -> PathComponent:
|
||||||
|
+ """Like ``_to_path_component`` but reject the empty result.
|
||||||
|
+
|
||||||
|
+ Use where a file is about to be written, so a missing name is an error
|
||||||
|
+ rather than a silent fallback to the directory itself.
|
||||||
|
+ """
|
||||||
|
+ component = _to_path_component(name)
|
||||||
|
+ if not component:
|
||||||
|
+ raise ValueError(f"Unexpected file name derived from URL: {name!r}")
|
||||||
|
+
|
||||||
|
+ return component
|
||||||
|
+
|
||||||
|
+
|
||||||
|
+def join_within_directory(directory: str, component: PathComponent) -> str:
|
||||||
|
+ """Join a single path ``component`` onto ``directory``.
|
||||||
|
+
|
||||||
|
+ ``component`` is a :data:`PathComponent`, so by type it has no separator and
|
||||||
|
+ is not a ``.`` or ``..`` reference; the result can never escape ``directory``.
|
||||||
|
+ Requiring ``PathComponent`` rather than ``str`` lets the type checker enforce
|
||||||
|
+ at the call site that the name was reduced to a safe component beforehand.
|
||||||
|
+ """
|
||||||
|
+ return os.path.join(directory, component)
|
||||||
|
+
|
||||||
|
+
|
||||||
|
# Order matters, earlier hashes have a precedence over later hashes for what
|
||||||
|
# we will pick to use.
|
||||||
|
_SUPPORTED_HASHES = ("sha512", "sha384", "sha256", "sha224", "sha1", "md5")
|
||||||
|
@@ -424,18 +468,13 @@ def redacted_url(self) -> str:
|
||||||
|
return redact_auth_from_url(self.url)
|
||||||
|
|
||||||
|
@property
|
||||||
|
- def filename(self) -> str:
|
||||||
|
- path = self.path.rstrip("/")
|
||||||
|
- name = posixpath.basename(path)
|
||||||
|
- if not name:
|
||||||
|
- # Make sure we don't leak auth information if the netloc
|
||||||
|
- # includes a username and password.
|
||||||
|
- netloc, user_pass = split_auth_from_netloc(self.netloc)
|
||||||
|
- return netloc
|
||||||
|
-
|
||||||
|
- name = urllib.parse.unquote(name)
|
||||||
|
- assert name, f"URL {self._url!r} produced no filename"
|
||||||
|
- return name
|
||||||
|
+ def filename(self) -> PathComponent:
|
||||||
|
+ name = _to_path_component(posixpath.basename(self.path.rstrip("/")))
|
||||||
|
+ if name:
|
||||||
|
+ return name
|
||||||
|
+
|
||||||
|
+ # No component in the path; fall back to the netloc, dropping any auth.
|
||||||
|
+ return _to_path_component(split_auth_from_netloc(self.netloc)[0])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def file_path(self) -> str:
|
||||||
|
diff --git a/src/pip/_internal/network/download.py b/src/pip/_internal/network/download.py
|
||||||
|
index 039b268878..6faafb5cb0 100644
|
||||||
|
--- a/src/pip/_internal/network/download.py
|
||||||
|
+++ b/src/pip/_internal/network/download.py
|
||||||
|
@@ -19,7 +19,12 @@
|
||||||
|
|
||||||
|
from pip._internal.cli.progress_bars import BarType, get_download_progress_renderer
|
||||||
|
from pip._internal.exceptions import IncompleteDownloadError, NetworkConnectionError
|
||||||
|
-from pip._internal.models.link import Link
|
||||||
|
+from pip._internal.models.link import (
|
||||||
|
+ Link,
|
||||||
|
+ PathComponent,
|
||||||
|
+ as_path_component,
|
||||||
|
+ join_within_directory,
|
||||||
|
+)
|
||||||
|
from pip._internal.network.cache import SafeFileCache, is_from_cache
|
||||||
|
from pip._internal.network.session import CacheControlAdapter, PipSession
|
||||||
|
from pip._internal.network.utils import HEADERS, raise_for_status, response_chunks
|
||||||
|
@@ -121,11 +126,14 @@ def parse_content_disposition(content_disposition: str, default_filename: str) -
|
||||||
|
return filename or default_filename
|
||||||
|
|
||||||
|
|
||||||
|
-def _get_http_response_filename(resp: Response, link: Link) -> str:
|
||||||
|
+def _get_http_response_filename(resp: Response, link: Link) -> PathComponent:
|
||||||
|
"""Get an ideal filename from the given HTTP response, falling back to
|
||||||
|
the link filename if not provided.
|
||||||
|
+
|
||||||
|
+ The result is validated as a single path component, so it can be joined onto
|
||||||
|
+ a download directory without escaping it.
|
||||||
|
"""
|
||||||
|
- filename = link.filename # fallback
|
||||||
|
+ filename: str = link.filename # fallback
|
||||||
|
# Have a look at the Content-Disposition header for a better guess
|
||||||
|
content_disposition = resp.headers.get("content-disposition")
|
||||||
|
if content_disposition:
|
||||||
|
@@ -139,7 +147,7 @@ def _get_http_response_filename(resp: Response, link: Link) -> str:
|
||||||
|
ext = os.path.splitext(resp.url)[1]
|
||||||
|
if ext:
|
||||||
|
filename += ext
|
||||||
|
- return filename
|
||||||
|
+ return as_path_component(filename)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
@@ -192,7 +200,9 @@ def __call__(self, link: Link, location: str) -> tuple[str, str]:
|
||||||
|
resp = self._http_get(link)
|
||||||
|
download_size = _get_http_response_size(resp)
|
||||||
|
|
||||||
|
- filepath = os.path.join(location, _get_http_response_filename(resp, link))
|
||||||
|
+ filepath = join_within_directory(
|
||||||
|
+ location, _get_http_response_filename(resp, link)
|
||||||
|
+ )
|
||||||
|
with open(filepath, "wb") as content_file:
|
||||||
|
download = _FileDownload(link, content_file, download_size)
|
||||||
|
self._process_response(download, resp)
|
||||||
|
diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py
|
||||||
|
index afcc0376da..3b44403e0d 100644
|
||||||
|
--- a/src/pip/_internal/operations/prepare.py
|
||||||
|
+++ b/src/pip/_internal/operations/prepare.py
|
||||||
|
@@ -29,7 +29,7 @@
|
||||||
|
from pip._internal.index.package_finder import PackageFinder
|
||||||
|
from pip._internal.metadata import BaseDistribution, get_metadata_distribution
|
||||||
|
from pip._internal.models.direct_url import ArchiveInfo, DirectUrl
|
||||||
|
-from pip._internal.models.link import Link
|
||||||
|
+from pip._internal.models.link import Link, join_within_directory
|
||||||
|
from pip._internal.models.wheel import Wheel
|
||||||
|
from pip._internal.network.download import Downloader
|
||||||
|
from pip._internal.network.lazy_wheel import (
|
||||||
|
@@ -201,7 +201,7 @@ def _check_download_dir(
|
||||||
|
"""Check download_dir for previously downloaded file with correct hash
|
||||||
|
If a correct file is found return its path else None
|
||||||
|
"""
|
||||||
|
- download_path = os.path.join(download_dir, link.filename)
|
||||||
|
+ download_path = join_within_directory(download_dir, link.filename)
|
||||||
|
|
||||||
|
if not os.path.exists(download_path):
|
||||||
|
return None
|
||||||
|
@@ -687,7 +687,7 @@ def save_linked_requirement(self, req: InstallRequirement) -> None:
|
||||||
|
# No distribution was downloaded for this requirement.
|
||||||
|
return
|
||||||
|
|
||||||
|
- download_location = os.path.join(self.download_dir, link.filename)
|
||||||
|
+ download_location = join_within_directory(self.download_dir, link.filename)
|
||||||
|
if not os.path.exists(download_location):
|
||||||
|
shutil.copy(req.local_file_path, download_location)
|
||||||
|
download_path = display_path(download_location)
|
||||||
|
diff --git a/tests/unit/test_link.py b/tests/unit/test_link.py
|
||||||
|
index c49f8547ac..bc8cb8ab9b 100644
|
||||||
|
--- a/tests/unit/test_link.py
|
||||||
|
+++ b/tests/unit/test_link.py
|
||||||
|
@@ -1,9 +1,17 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
+import os
|
||||||
|
+import posixpath
|
||||||
|
+
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from pip._internal.exceptions import InvalidEggFragment, PipError
|
||||||
|
-from pip._internal.models.link import Link, links_equivalent
|
||||||
|
+from pip._internal.models.link import (
|
||||||
|
+ Link,
|
||||||
|
+ as_path_component,
|
||||||
|
+ join_within_directory,
|
||||||
|
+ links_equivalent,
|
||||||
|
+)
|
||||||
|
from pip._internal.utils.hashes import Hashes
|
||||||
|
|
||||||
|
|
||||||
|
@@ -29,6 +37,13 @@ def test_repr(self, url: str, expected: str) -> None:
|
||||||
|
("https://example.com/path/page.html", "page.html"),
|
||||||
|
# Test a quoted character.
|
||||||
|
("https://example.com/path/page%231.html", "page#1.html"),
|
||||||
|
+ # A doubly-encoded separator must stay encoded: the path is decoded
|
||||||
|
+ # exactly once, so the file name keeps its literal "%2F" instead of
|
||||||
|
+ # collapsing into a "/".
|
||||||
|
+ (
|
||||||
|
+ "https://example.com/a%252Fb.whl",
|
||||||
|
+ "a%2Fb.whl",
|
||||||
|
+ ),
|
||||||
|
(
|
||||||
|
"http://yo/myproject-1.0%2Bfoobar.0-py2.py3-none-any.whl",
|
||||||
|
"myproject-1.0+foobar.0-py2.py3-none-any.whl",
|
||||||
|
@@ -49,6 +64,52 @@ def test_filename(self, url: str, expected: str) -> None:
|
||||||
|
link = Link(url)
|
||||||
|
assert link.filename == expected
|
||||||
|
|
||||||
|
+ @pytest.mark.parametrize(
|
||||||
|
+ "url",
|
||||||
|
+ [
|
||||||
|
+ "https://example.com/a%252Fb.whl",
|
||||||
|
+ "https://example.com/%252e%252e%252fb.whl",
|
||||||
|
+ ],
|
||||||
|
+ )
|
||||||
|
+ def test_filename_decoded_once_stays_single_component(self, url: str) -> None:
|
||||||
|
+ # The path is decoded exactly once, so an encoded separator stays
|
||||||
|
+ # encoded and the file name remains a single path component rather
|
||||||
|
+ # than collapsing into a "/"-separated path.
|
||||||
|
+ filename = Link(url).filename
|
||||||
|
+ assert not posixpath.isabs(filename)
|
||||||
|
+ assert posixpath.basename(filename) == filename
|
||||||
|
+
|
||||||
|
+ @pytest.mark.parametrize(
|
||||||
|
+ "url",
|
||||||
|
+ [
|
||||||
|
+ "https://example.com/..",
|
||||||
|
+ "https://example.com/.",
|
||||||
|
+ "https://example.com/foo/%2e%2e",
|
||||||
|
+ ],
|
||||||
|
+ )
|
||||||
|
+ def test_filename_parent_reference_falls_back_to_netloc(self, url: str) -> None:
|
||||||
|
+ # A path that is only a "." or ".." reference has no usable file name,
|
||||||
|
+ # so filename falls back to the netloc rather than handing back a
|
||||||
|
+ # traversal component that could escape a download directory.
|
||||||
|
+ assert Link(url).filename == "example.com"
|
||||||
|
+
|
||||||
|
+ @pytest.mark.parametrize(
|
||||||
|
+ "url",
|
||||||
|
+ [
|
||||||
|
+ # A path-less URL whose authority looks like a traversal: the netloc
|
||||||
|
+ # fallback must still reduce to a single path component.
|
||||||
|
+ "http://..\\..\\..\\evil.whl",
|
||||||
|
+ "http://../",
|
||||||
|
+ "http://..",
|
||||||
|
+ ],
|
||||||
|
+ )
|
||||||
|
+ def test_filename_is_always_a_path_component(self, url: str) -> None:
|
||||||
|
+ # filename must never carry a separator or parent reference, so joining
|
||||||
|
+ # it onto a directory can never escape that directory.
|
||||||
|
+ name = Link(url).filename
|
||||||
|
+ assert os.path.basename(name) == name
|
||||||
|
+ assert name not in (os.curdir, os.pardir)
|
||||||
|
+
|
||||||
|
def test_splitext(self) -> None:
|
||||||
|
assert ("wheel", ".whl") == Link("http://yo/wheel.whl").splitext()
|
||||||
|
|
||||||
|
@@ -244,3 +305,53 @@ def test_links_equivalent(url1: str, url2: str) -> None:
|
||||||
|
)
|
||||||
|
def test_links_equivalent_false(url1: str, url2: str) -> None:
|
||||||
|
assert not links_equivalent(Link(url1), Link(url2))
|
||||||
|
+
|
||||||
|
+
|
||||||
|
+@pytest.mark.parametrize(
|
||||||
|
+ "name",
|
||||||
|
+ [
|
||||||
|
+ "wheel.whl",
|
||||||
|
+ "myproject-1.0+foobar.0-py2.py3-none-any.whl",
|
||||||
|
+ # A literal "%2F" is a normal file name, not a separator.
|
||||||
|
+ "a%2Fb.whl",
|
||||||
|
+ ],
|
||||||
|
+)
|
||||||
|
+def test_as_path_component_keeps_plain_name(name: str) -> None:
|
||||||
|
+ assert as_path_component(name) == name
|
||||||
|
+
|
||||||
|
+
|
||||||
|
+@pytest.mark.parametrize(
|
||||||
|
+ "name",
|
||||||
|
+ [
|
||||||
|
+ os.path.join(os.sep, "abs", "pkg.whl"),
|
||||||
|
+ os.path.join("..", "pkg.whl"),
|
||||||
|
+ os.path.join("nested", "pkg.whl"),
|
||||||
|
+ ],
|
||||||
|
+)
|
||||||
|
+def test_as_path_component_reduces_to_basename(name: str) -> None:
|
||||||
|
+ # A name carrying directory components is reduced to its basename, so the
|
||||||
|
+ # result always stays inside the directory it is later joined onto.
|
||||||
|
+ assert as_path_component(name) == os.path.basename(name)
|
||||||
|
+
|
||||||
|
+
|
||||||
|
+@pytest.mark.parametrize("name", ["", ".", "..", "/", os.path.join("sub", "..")])
|
||||||
|
+def test_as_path_component_rejects_empty_or_parent_reference(name: str) -> None:
|
||||||
|
+ with pytest.raises(ValueError):
|
||||||
|
+ as_path_component(name)
|
||||||
|
+
|
||||||
|
+
|
||||||
|
+@pytest.mark.parametrize(
|
||||||
|
+ "name",
|
||||||
|
+ [
|
||||||
|
+ "pkg.whl",
|
||||||
|
+ # A literal "%2F" is a normal file name, not a separator.
|
||||||
|
+ "a%2Fb.whl",
|
||||||
|
+ ],
|
||||||
|
+)
|
||||||
|
+def test_join_within_directory_stays_inside(name: str) -> None:
|
||||||
|
+ # The component is joined onto the directory as its final element, so the
|
||||||
|
+ # result stays inside the directory.
|
||||||
|
+ directory = os.path.join("base", "downloads")
|
||||||
|
+ joined = join_within_directory(directory, as_path_component(name))
|
||||||
|
+ assert joined == os.path.join(directory, name)
|
||||||
|
+ assert os.path.basename(joined) == name
|
||||||
29
4c6d7471de.patch
Normal file
29
4c6d7471de.patch
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
From 4c6d7471dec62fb004a47a7c2164b6b5b089ac06 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Richard Si <sichard26@gmail.com>
|
||||||
|
Date: Fri, 5 Jun 2026 15:44:14 -0400
|
||||||
|
Subject: [PATCH] Also fix user site patching in test suite
|
||||||
|
|
||||||
|
---
|
||||||
|
tests/lib/venv.py | 6 ++++--
|
||||||
|
1 file changed, 4 insertions(+), 2 deletions(-)
|
||||||
|
|
||||||
|
diff --git a/tests/lib/venv.py b/tests/lib/venv.py
|
||||||
|
index 67b01d9f31..4e86b92b3b 100644
|
||||||
|
--- a/tests/lib/venv.py
|
||||||
|
+++ b/tests/lib/venv.py
|
||||||
|
@@ -174,11 +174,13 @@ def _customize_site(self) -> None:
|
||||||
|
site.ENABLE_USER_SITE = {self._user_site_packages}
|
||||||
|
# First, drop system-sites related paths.
|
||||||
|
original_sys_path = sys.path[:]
|
||||||
|
+ # To discover system-sites related paths, clear sys.path
|
||||||
|
+ # and build a new one with only system paths.
|
||||||
|
+ sys.path = []
|
||||||
|
known_paths = set()
|
||||||
|
for path in site.getsitepackages():
|
||||||
|
site.addsitedir(path, known_paths=known_paths)
|
||||||
|
- system_paths = sys.path[len(original_sys_path):]
|
||||||
|
- for path in system_paths:
|
||||||
|
+ for path in sys.path:
|
||||||
|
if path in original_sys_path:
|
||||||
|
original_sys_path.remove(path)
|
||||||
|
sys.path = original_sys_path
|
||||||
62
6099a54ddd.patch
Normal file
62
6099a54ddd.patch
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
From 6099a54dddbfbc7fb912d53b6adad5ff6b8d1745 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Richard Si <sichard26@gmail.com>
|
||||||
|
Date: Fri, 5 Jun 2026 15:03:38 -0400
|
||||||
|
Subject: [PATCH] Fix sitecustomize.py used for build isolation on Python 3.15+
|
||||||
|
|
||||||
|
The sitecustomize.py file pip uses to isolate build subprocesses from
|
||||||
|
the parent environment discovers system related paths by calling
|
||||||
|
site.addsitedir() for every system site-packages path and observing
|
||||||
|
what new entries are appended to sys.path.
|
||||||
|
|
||||||
|
This breaks since Python 3.15b2 due to two changes:
|
||||||
|
|
||||||
|
- site.addsitedir() won't add a path if it already exists in sys.path
|
||||||
|
|
||||||
|
- site.addsitedir() won't re-execute .pth files if called for a known
|
||||||
|
directory (which includes the system sites because known_path is
|
||||||
|
mutated by addsitedir before it checks for .pth files)
|
||||||
|
|
||||||
|
To cope with this, temporarily clear sys.path before using
|
||||||
|
site.addsitedir() to discover all system paths for exclusion.
|
||||||
|
---
|
||||||
|
news/14033.bugfix.rst | 1 +
|
||||||
|
src/pip/_internal/build_env.py | 14 +++++++++-----
|
||||||
|
2 files changed, 10 insertions(+), 5 deletions(-)
|
||||||
|
create mode 100644 news/14033.bugfix.rst
|
||||||
|
|
||||||
|
diff --git a/news/14033.bugfix.rst b/news/14033.bugfix.rst
|
||||||
|
new file mode 100644
|
||||||
|
index 0000000000..404196a2f0
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/news/14033.bugfix.rst
|
||||||
|
@@ -0,0 +1 @@
|
||||||
|
+Prevent system packages from leaking into isolated build environments on Python 3.15
|
||||||
|
diff --git a/src/pip/_internal/build_env.py b/src/pip/_internal/build_env.py
|
||||||
|
index 1a42a9d411..7639dabcad 100644
|
||||||
|
--- a/src/pip/_internal/build_env.py
|
||||||
|
+++ b/src/pip/_internal/build_env.py
|
||||||
|
@@ -468,15 +468,19 @@ def __init__(self, installer: BuildEnvironmentInstaller) -> None:
|
||||||
|
"""
|
||||||
|
import os, site, sys
|
||||||
|
|
||||||
|
- # First, drop system-sites related paths.
|
||||||
|
+ # First, discover all system-sites related paths.
|
||||||
|
original_sys_path = sys.path[:]
|
||||||
|
+ # Clear sys.path so addsitedir() will add system site paths and paths
|
||||||
|
+ # added by contained .pth files to sys.path reliably. This is necessary
|
||||||
|
+ # since Python 3.15, which notably no longer re-executes .pth files for
|
||||||
|
+ # known paths.
|
||||||
|
+ sys.path = []
|
||||||
|
known_paths = set()
|
||||||
|
for path in {system_sites!r}:
|
||||||
|
site.addsitedir(path, known_paths=known_paths)
|
||||||
|
- system_paths = set(
|
||||||
|
- os.path.normcase(path)
|
||||||
|
- for path in sys.path[len(original_sys_path):]
|
||||||
|
- )
|
||||||
|
+ system_paths = set(os.path.normcase(path) for path in sys.path)
|
||||||
|
+
|
||||||
|
+ # Drop discovered system-sites related paths.
|
||||||
|
original_sys_path = [
|
||||||
|
path for path in original_sys_path
|
||||||
|
if os.path.normcase(path) not in system_paths
|
||||||
34
plan.fmf
34
plan.fmf
|
|
@ -6,12 +6,17 @@ discover:
|
||||||
how: shell
|
how: shell
|
||||||
url: https://src.fedoraproject.org/tests/python.git
|
url: https://src.fedoraproject.org/tests/python.git
|
||||||
tests:
|
tests:
|
||||||
- name: smoke36
|
- name: smoke
|
||||||
path: /smoke
|
path: /smoke
|
||||||
test: VERSION=3.6 TOX=false ./venv.sh
|
test: ./venv.sh
|
||||||
- name: smoke39
|
- name: smoke_virtualenv
|
||||||
path: /smoke
|
path: /smoke
|
||||||
test: VERSION=3.9 ./venv.sh
|
test: METHOD=virtualenv ./venv.sh
|
||||||
|
- name: tests_python_fedora_only
|
||||||
|
how: shell
|
||||||
|
url: https://src.fedoraproject.org/tests/python.git
|
||||||
|
when: distro != fedora-eln and distro == fedora
|
||||||
|
tests:
|
||||||
- name: smoke310
|
- name: smoke310
|
||||||
path: /smoke
|
path: /smoke
|
||||||
test: VERSION=3.10 ./venv.sh
|
test: VERSION=3.10 ./venv.sh
|
||||||
|
|
@ -30,9 +35,6 @@ discover:
|
||||||
- name: smoke315
|
- name: smoke315
|
||||||
path: /smoke
|
path: /smoke
|
||||||
test: VERSION=3.15 ./venv.sh
|
test: VERSION=3.15 ./venv.sh
|
||||||
- name: smoke39_virtualenv
|
|
||||||
path: /smoke
|
|
||||||
test: VERSION=3.9 METHOD=virtualenv ./venv.sh
|
|
||||||
- name: smoke310_virtualenv
|
- name: smoke310_virtualenv
|
||||||
path: /smoke
|
path: /smoke
|
||||||
test: VERSION=3.10 METHOD=virtualenv ./venv.sh
|
test: VERSION=3.10 METHOD=virtualenv ./venv.sh
|
||||||
|
|
@ -88,14 +90,6 @@ prepare:
|
||||||
package:
|
package:
|
||||||
- gcc
|
- gcc
|
||||||
- virtualenv
|
- virtualenv
|
||||||
- python3.6-devel
|
|
||||||
- python3.9-devel
|
|
||||||
- python3.10-devel
|
|
||||||
- python3.11-devel
|
|
||||||
- python3.12-devel
|
|
||||||
- python3.13-devel
|
|
||||||
- python3.14-devel
|
|
||||||
- python3.15-devel
|
|
||||||
- python3-devel
|
- python3-devel
|
||||||
- python3-tox
|
- python3-tox
|
||||||
- mock
|
- mock
|
||||||
|
|
@ -106,6 +100,16 @@ prepare:
|
||||||
- shadow-utils
|
- shadow-utils
|
||||||
- expect
|
- expect
|
||||||
- dnf
|
- dnf
|
||||||
|
- name: Install dependencies (Fedora not ELN)
|
||||||
|
how: install
|
||||||
|
when: distro != fedora-eln and distro == fedora
|
||||||
|
package:
|
||||||
|
- python3.10-devel
|
||||||
|
- python3.11-devel
|
||||||
|
- python3.12-devel
|
||||||
|
- python3.13-devel
|
||||||
|
- python3.14-devel
|
||||||
|
- python3.15-devel
|
||||||
- name: Update packages
|
- name: Update packages
|
||||||
how: shell
|
how: shell
|
||||||
script: dnf upgrade -y
|
script: dnf upgrade -y
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
%bcond man 1
|
%bcond man 1
|
||||||
|
|
||||||
%global srcname pip
|
%global srcname pip
|
||||||
%global base_version 26.0.1
|
%global base_version 26.1.2
|
||||||
%global upstream_version %{base_version}%{?prerel}
|
%global upstream_version %{base_version}%{?prerel}
|
||||||
%global python_wheel_name %{srcname}-%{upstream_version}-py3-none-any.whl
|
%global python_wheel_name %{srcname}-%{upstream_version}-py3-none-any.whl
|
||||||
|
|
||||||
|
|
@ -20,7 +20,6 @@ Summary: A tool for installing and managing Python packages
|
||||||
|
|
||||||
# certifi: MPL-2.0
|
# certifi: MPL-2.0
|
||||||
# CacheControl: Apache-2.0
|
# CacheControl: Apache-2.0
|
||||||
# dependency-groups: MIT
|
|
||||||
# distlib: Python-2.0.1
|
# distlib: Python-2.0.1
|
||||||
# distro: Apache-2.0
|
# distro: Apache-2.0
|
||||||
# idna: BSD-3-Clause
|
# idna: BSD-3-Clause
|
||||||
|
|
@ -96,10 +95,24 @@ Patch: dummy-certifi.patch
|
||||||
# We don't need a layer to check that, as we're by default in an offline environment
|
# We don't need a layer to check that, as we're by default in an offline environment
|
||||||
Patch: downstream-remove-pytest-subket.patch
|
Patch: downstream-remove-pytest-subket.patch
|
||||||
|
|
||||||
# Patch for the bundled urllib3 for CVE-2025-50181
|
# Fix sitecustomize.py used for build isolation on Python 3.15+
|
||||||
# Redirects are not disabled when retries are disabled on PoolManager instantiation
|
Patch: https://github.com/pypa/pip/commit/6099a54ddd.patch
|
||||||
# Upstream fix: https://github.com/urllib3/urllib3/commit/f05b1329126d5be6de501f9d1e3e36738bc08857
|
|
||||||
Patch: urllib3-CVE-2025-50181.patch
|
# Fix user-site path ordering in the test suite on Python 3.15+
|
||||||
|
# The same CPython gh-149819 change that broke build env isolation also broke
|
||||||
|
# _customize_site() in tests/lib/venv.py: site.addsitedir() no longer
|
||||||
|
# re-appends paths already in sys.path, so the detection of system-site paths
|
||||||
|
# produces an empty list and user site ends up after venv site-packages instead
|
||||||
|
# of before it, causing user-site install/uninstall tests to operate on the
|
||||||
|
# wrong installation.
|
||||||
|
Patch: https://github.com/pypa/pip/commit/4c6d7471de.patch
|
||||||
|
|
||||||
|
# Allow flit-core 4 to build pip
|
||||||
|
# https://github.com/pypa/pip/commit/09a03f6cfa (non-existing files removed)
|
||||||
|
Patch: 09a03f6cfa.patch
|
||||||
|
|
||||||
|
# CVE-2026-13346: Link.filename double URL decode allows path traversal
|
||||||
|
Patch: https://github.com/pypa/pip/commit/10dfb6b900.patch
|
||||||
|
|
||||||
# Remove -s from Python shebang - ensure that packages installed with pip
|
# Remove -s from Python shebang - ensure that packages installed with pip
|
||||||
# to user locations are seen by pip itself
|
# to user locations are seen by pip itself
|
||||||
|
|
@ -118,24 +131,23 @@ Packages" or "Pip Installs Python".
|
||||||
# %%{_rpmconfigdir}/pythonbundles.py --namespace 'python%%{1}dist' src/pip/_vendor/vendor.txt
|
# %%{_rpmconfigdir}/pythonbundles.py --namespace 'python%%{1}dist' src/pip/_vendor/vendor.txt
|
||||||
%global bundled() %{expand:
|
%global bundled() %{expand:
|
||||||
Provides: bundled(python%{1}dist(cachecontrol)) = 0.14.4
|
Provides: bundled(python%{1}dist(cachecontrol)) = 0.14.4
|
||||||
Provides: bundled(python%{1}dist(certifi)) = 2026.1.4
|
Provides: bundled(python%{1}dist(certifi)) = 2026.2.25
|
||||||
Provides: bundled(python%{1}dist(dependency-groups)) = 1.3.1
|
|
||||||
Provides: bundled(python%{1}dist(distlib)) = 0.4
|
Provides: bundled(python%{1}dist(distlib)) = 0.4
|
||||||
Provides: bundled(python%{1}dist(distro)) = 1.9
|
Provides: bundled(python%{1}dist(distro)) = 1.9
|
||||||
Provides: bundled(python%{1}dist(idna)) = 3.11
|
Provides: bundled(python%{1}dist(idna)) = 3.11
|
||||||
Provides: bundled(python%{1}dist(msgpack)) = 1.1.2
|
Provides: bundled(python%{1}dist(msgpack)) = 1.1.2
|
||||||
Provides: bundled(python%{1}dist(packaging)) = 26
|
Provides: bundled(python%{1}dist(packaging)) = 26.2
|
||||||
Provides: bundled(python%{1}dist(platformdirs)) = 4.5.1
|
Provides: bundled(python%{1}dist(platformdirs)) = 4.5.1
|
||||||
Provides: bundled(python%{1}dist(pygments)) = 2.19.2
|
Provides: bundled(python%{1}dist(pygments)) = 2.19.2
|
||||||
Provides: bundled(python%{1}dist(pyproject-hooks)) = 1.2
|
Provides: bundled(python%{1}dist(pyproject-hooks)) = 1.2
|
||||||
Provides: bundled(python%{1}dist(requests)) = 2.32.5
|
Provides: bundled(python%{1}dist(requests)) = 2.33.1
|
||||||
Provides: bundled(python%{1}dist(resolvelib)) = 1.2.1
|
Provides: bundled(python%{1}dist(resolvelib)) = 1.2.1
|
||||||
Provides: bundled(python%{1}dist(rich)) = 14.2
|
Provides: bundled(python%{1}dist(rich)) = 14.2
|
||||||
Provides: bundled(python%{1}dist(setuptools)) = 70.3
|
Provides: bundled(python%{1}dist(setuptools)) = 70.3
|
||||||
Provides: bundled(python%{1}dist(tomli)) = 2.3
|
Provides: bundled(python%{1}dist(tomli)) = 2.3.1
|
||||||
Provides: bundled(python%{1}dist(tomli-w)) = 1.2
|
Provides: bundled(python%{1}dist(tomli-w)) = 1.2
|
||||||
Provides: bundled(python%{1}dist(truststore)) = 0.10.4
|
Provides: bundled(python%{1}dist(truststore)) = 0.10.4
|
||||||
Provides: bundled(python%{1}dist(urllib3)) = 1.26.20
|
Provides: bundled(python%{1}dist(urllib3)) = 2.6.3
|
||||||
}
|
}
|
||||||
|
|
||||||
# Some manylinux1 wheels need libcrypt.so.1.
|
# Some manylinux1 wheels need libcrypt.so.1.
|
||||||
|
|
@ -177,7 +189,9 @@ Requires: ca-certificates
|
||||||
# Virtual provides for the packages bundled by pip:
|
# Virtual provides for the packages bundled by pip:
|
||||||
%{bundled %{python3_pkgversion}}
|
%{bundled %{python3_pkgversion}}
|
||||||
|
|
||||||
|
%if "%{python3_pkgversion}" == "3"
|
||||||
Provides: pip = %{version}-%{release}
|
Provides: pip = %{version}-%{release}
|
||||||
|
%endif
|
||||||
|
|
||||||
%description -n python%{python3_pkgversion}-%{srcname}
|
%description -n python%{python3_pkgversion}-%{srcname}
|
||||||
pip is a package management system used to install and manage software packages
|
pip is a package management system used to install and manage software packages
|
||||||
|
|
@ -291,7 +305,7 @@ sed -i -e "s/^\\(complete.*\\) pip%{python3_version}\$/\\1 pip%{python3_version}
|
||||||
|
|
||||||
# Install the built wheel and inject SBOM into it (if the macro is available)
|
# Install the built wheel and inject SBOM into it (if the macro is available)
|
||||||
mkdir -p %{buildroot}%{python_wheel_dir}
|
mkdir -p %{buildroot}%{python_wheel_dir}
|
||||||
install -p %{_pyproject_wheeldir}/%{python_wheel_name} -t %{buildroot}%{python_wheel_dir}
|
install -pm0644 %{_pyproject_wheeldir}/%{python_wheel_name} -t %{buildroot}%{python_wheel_dir}
|
||||||
%{?python_wheel_inject_sbom:%python_wheel_inject_sbom %{buildroot}%{python_wheel_dir}/%{python_wheel_name}}
|
%{?python_wheel_inject_sbom:%python_wheel_inject_sbom %{buildroot}%{python_wheel_dir}/%{python_wheel_name}}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -314,9 +328,8 @@ grep "pem$" %{pyproject_files} && exit 1 || true
|
||||||
pytest_k='not completion'
|
pytest_k='not completion'
|
||||||
# this clashes with our PYTHONPATH
|
# this clashes with our PYTHONPATH
|
||||||
pytest_k="$pytest_k and not environments_with_no_pip"
|
pytest_k="$pytest_k and not environments_with_no_pip"
|
||||||
# this seems to require internet (despite no network marker)
|
# this requires internet without the keyring local wheel
|
||||||
# added in https://github.com/pypa/pip/pull/13378 TODO drop this in the next release
|
pytest_k="$pytest_k and not test_prompt_for_keyring_if_needed"
|
||||||
pytest_k="$pytest_k and not test_prompt_for_keyring_if_needed and not test_double_install_fail and not test_install_sdist_links and not test_lock_vcs and not test_lock_archive and not test_backend_sees_config_via_sdist"
|
|
||||||
# this cannot import breezy, TODO investigate
|
# this cannot import breezy, TODO investigate
|
||||||
pytest_k="$pytest_k and not (functional and bazaar)"
|
pytest_k="$pytest_k and not (functional and bazaar)"
|
||||||
# failures to investigate
|
# failures to investigate
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
From d5cb806a14bb50a96484bca6536e81f2ac316b9d Mon Sep 17 00:00:00 2001
|
From a12217cf8f9bf3ab8e39d9bcc6b42e7043e79b08 Mon Sep 17 00:00:00 2001
|
||||||
From: Karolina Surma <ksurma@redhat.com>
|
From: Karolina Surma <ksurma@redhat.com>
|
||||||
Date: Wed, 16 Feb 2022 08:36:21 +0100
|
Date: Wed, 16 Feb 2022 08:36:21 +0100
|
||||||
Subject: [PATCH] Prevent removing of the system packages installed under
|
Subject: [PATCH] Prevent removing of the system packages installed under
|
||||||
|
|
@ -53,10 +53,10 @@ index 230e114..8bd5d31 100644
|
||||||
class BaseEnvironment:
|
class BaseEnvironment:
|
||||||
"""An environment containing distributions to introspect."""
|
"""An environment containing distributions to introspect."""
|
||||||
diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py
|
diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py
|
||||||
index c9f6bff..c101826 100644
|
index aad2f0e..7b385b1 100644
|
||||||
--- a/src/pip/_internal/req/req_install.py
|
--- a/src/pip/_internal/req/req_install.py
|
||||||
+++ b/src/pip/_internal/req/req_install.py
|
+++ b/src/pip/_internal/req/req_install.py
|
||||||
@@ -453,7 +453,7 @@ class InstallRequirement:
|
@@ -439,7 +439,7 @@ class InstallRequirement:
|
||||||
f"lack sys.path precedence to {existing_dist.raw_name} "
|
f"lack sys.path precedence to {existing_dist.raw_name} "
|
||||||
f"in {existing_dist.location}"
|
f"in {existing_dist.location}"
|
||||||
)
|
)
|
||||||
|
|
@ -66,7 +66,7 @@ index c9f6bff..c101826 100644
|
||||||
else:
|
else:
|
||||||
if self.editable:
|
if self.editable:
|
||||||
diff --git a/src/pip/_internal/resolution/legacy/resolver.py b/src/pip/_internal/resolution/legacy/resolver.py
|
diff --git a/src/pip/_internal/resolution/legacy/resolver.py b/src/pip/_internal/resolution/legacy/resolver.py
|
||||||
index 33a4fdc..1fe886e 100644
|
index 6cc6311..4188ef1 100644
|
||||||
--- a/src/pip/_internal/resolution/legacy/resolver.py
|
--- a/src/pip/_internal/resolution/legacy/resolver.py
|
||||||
+++ b/src/pip/_internal/resolution/legacy/resolver.py
|
+++ b/src/pip/_internal/resolution/legacy/resolver.py
|
||||||
@@ -322,7 +322,9 @@ class Resolver(BaseResolver):
|
@@ -322,7 +322,9 @@ class Resolver(BaseResolver):
|
||||||
|
|
@ -81,11 +81,11 @@ index 33a4fdc..1fe886e 100644
|
||||||
req.satisfied_by = None
|
req.satisfied_by = None
|
||||||
|
|
||||||
diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py
|
diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py
|
||||||
index f23e4cd..1bada79 100644
|
index a74200a..99738cc 100644
|
||||||
--- a/src/pip/_internal/resolution/resolvelib/factory.py
|
--- a/src/pip/_internal/resolution/resolvelib/factory.py
|
||||||
+++ b/src/pip/_internal/resolution/resolvelib/factory.py
|
+++ b/src/pip/_internal/resolution/resolvelib/factory.py
|
||||||
@@ -3,6 +3,8 @@ from __future__ import annotations
|
@@ -4,6 +4,8 @@ import contextlib
|
||||||
import contextlib
|
import copy
|
||||||
import functools
|
import functools
|
||||||
import logging
|
import logging
|
||||||
+import sys
|
+import sys
|
||||||
|
|
@ -93,7 +93,7 @@ index f23e4cd..1bada79 100644
|
||||||
from collections.abc import Iterable, Iterator, Mapping, Sequence
|
from collections.abc import Iterable, Iterator, Mapping, Sequence
|
||||||
from typing import (
|
from typing import (
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
@@ -615,6 +617,16 @@ class Factory:
|
@@ -674,6 +676,16 @@ class Factory:
|
||||||
if dist is None: # Not installed, no uninstallation required.
|
if dist is None: # Not installed, no uninstallation required.
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
@ -111,5 +111,5 @@ index f23e4cd..1bada79 100644
|
||||||
# be uninstalled, no matter it's in global or user site, because the
|
# be uninstalled, no matter it's in global or user site, because the
|
||||||
# user site installation has precedence over global.
|
# user site installation has precedence over global.
|
||||||
--
|
--
|
||||||
2.50.1
|
2.54.0
|
||||||
|
|
||||||
|
|
|
||||||
2
sources
2
sources
|
|
@ -1,4 +1,4 @@
|
||||||
SHA512 (pip-26.0.1.tar.gz) = 3786df7522ea65bc20fb9885ce5c2ddc60200a536a1f754a8d7dc278115c73258863e4c51ac7e9a60dda0b70263730dc194f70e1e8f8d00178a8b3c724333bf0
|
|
||||||
SHA512 (setuptools-79.0.1-py3-none-any.whl) = fef6cfc6f95a5bb7320f1680e1c665cb8d9a4e4227cde4d8aab8a50bed4bcf04320085b9d7d5343359f887008db5c5a861e57f3d08b7b0b2311a28adaeee6b4a
|
SHA512 (setuptools-79.0.1-py3-none-any.whl) = fef6cfc6f95a5bb7320f1680e1c665cb8d9a4e4227cde4d8aab8a50bed4bcf04320085b9d7d5343359f887008db5c5a861e57f3d08b7b0b2311a28adaeee6b4a
|
||||||
SHA512 (flit_core-3.12.0-py3-none-any.whl) = 790c12b1f43201e365fb3f8f2f0a54e1a578876799dfdf8bfeea679a25ea096bf62946d006618c1458ae6e37ce6d00998f37e9aba426d5ab80d32ef2d75da4e0
|
SHA512 (flit_core-3.12.0-py3-none-any.whl) = 790c12b1f43201e365fb3f8f2f0a54e1a578876799dfdf8bfeea679a25ea096bf62946d006618c1458ae6e37ce6d00998f37e9aba426d5ab80d32ef2d75da4e0
|
||||||
|
SHA512 (pip-26.1.2.tar.gz) = e29c98a7da5e329183b7eef86a66f9d6c3473051f64aa6e762714306148547eb0de4220824484071822a9a62bd01a62a09ab16bba4c26e4b847bfc2609728608
|
||||||
SHA512 (coverage-0-py3-none-any.whl) = e734192565347010efe68f8ba600254259c9b647f3c553fd4e5d87b1d7f955cb15d6f7d807716f4a6415d239beed945fbec7210feaf502e9cc849c332845926e
|
SHA512 (coverage-0-py3-none-any.whl) = e734192565347010efe68f8ba600254259c9b647f3c553fd4e5d87b1d7f955cb15d6f7d807716f4a6415d239beed945fbec7210feaf502e9cc849c332845926e
|
||||||
|
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
From b3d543d7e16af844394316360ef1bf0b9d10f1b1 Mon Sep 17 00:00:00 2001
|
|
||||||
From: Illia Volochii <illia.volochii@gmail.com>
|
|
||||||
Date: Wed, 18 Jun 2025 16:25:01 +0300
|
|
||||||
Subject: [PATCH] Security fix for CVE-2025-50181
|
|
||||||
|
|
||||||
Co-authored-by: Seth Michael Larson <sethmichaellarson@gmail.com>
|
|
||||||
Co-authored-by: Quentin Pradet <quentin.pradet@gmail.com>
|
|
||||||
Co-authored-by: Seth Michael Larson <sethmichaellarson@gmail.com>
|
|
||||||
---
|
|
||||||
src/pip/_vendor/urllib3/poolmanager.py | 18 +++++++++++++++++-
|
|
||||||
1 file changed, 17 insertions(+), 1 deletion(-)
|
|
||||||
|
|
||||||
diff --git a/src/pip/_vendor/urllib3/poolmanager.py b/src/pip/_vendor/urllib3/poolmanager.py
|
|
||||||
index fb51bf7..a8de7c6 100644
|
|
||||||
--- a/src/pip/_vendor/urllib3/poolmanager.py
|
|
||||||
+++ b/src/pip/_vendor/urllib3/poolmanager.py
|
|
||||||
@@ -170,6 +170,22 @@ class PoolManager(RequestMethods):
|
|
||||||
|
|
||||||
def __init__(self, num_pools=10, headers=None, **connection_pool_kw):
|
|
||||||
RequestMethods.__init__(self, headers)
|
|
||||||
+ if "retries" in connection_pool_kw:
|
|
||||||
+ retries = connection_pool_kw["retries"]
|
|
||||||
+ if not isinstance(retries, Retry):
|
|
||||||
+ # When Retry is initialized, raise_on_redirect is based
|
|
||||||
+ # on a redirect boolean value.
|
|
||||||
+ # But requests made via a pool manager always set
|
|
||||||
+ # redirect to False, and raise_on_redirect always ends
|
|
||||||
+ # up being False consequently.
|
|
||||||
+ # Here we fix the issue by setting raise_on_redirect to
|
|
||||||
+ # a value needed by the pool manager without considering
|
|
||||||
+ # the redirect boolean.
|
|
||||||
+ raise_on_redirect = retries is not False
|
|
||||||
+ retries = Retry.from_int(retries, redirect=False)
|
|
||||||
+ retries.raise_on_redirect = raise_on_redirect
|
|
||||||
+ connection_pool_kw = connection_pool_kw.copy()
|
|
||||||
+ connection_pool_kw["retries"] = retries
|
|
||||||
self.connection_pool_kw = connection_pool_kw
|
|
||||||
self.pools = RecentlyUsedContainer(num_pools)
|
|
||||||
|
|
||||||
@@ -389,7 +405,7 @@ class PoolManager(RequestMethods):
|
|
||||||
kw["body"] = None
|
|
||||||
kw["headers"] = HTTPHeaderDict(kw["headers"])._prepare_for_method_change()
|
|
||||||
|
|
||||||
- retries = kw.get("retries")
|
|
||||||
+ retries = kw.get("retries", response.retries)
|
|
||||||
if not isinstance(retries, Retry):
|
|
||||||
retries = Retry.from_int(retries, redirect=redirect)
|
|
||||||
|
|
||||||
--
|
|
||||||
2.51.0
|
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue