Compare commits

..

13 commits

Author SHA1 Message Date
Miro Hrončok
e0d87e4172 Do not install the pip.whl with executable permissions
The wheel file is never executed, it has no shebang
(yes, zips can have shebangs).

    bash: /usr/share/python-wheels/pip-....whl: cannot execute binary file: Exec format error

This executable permission was never intended,
it only happens to be the default for the install command.
2026-08-31 12:45:22 +02:00
Lumir Balhar
ac655bc5de Security fix for CVE-2026-13346
Co-Authored-by: Miro Hrončok <miro@hroncok.cz>
2026-08-26 08:58:17 +02:00
Lukáš Zachar
08dc5bf450 CI: Make some test not running for ELN (and RHEL)
See also https://forge.fedoraproject.org/ci/tickets/issues/550

[skip changelog]
2026-08-25 14:23:39 +00:00
Miro Hrončok
c05c4a66bb Allow flit-core 4 for building this package 2026-08-20 20:16:00 +02:00
Fedora Release Engineering
47955c1bf6 Rebuilt for https://fedoraproject.org/wiki/Fedora_45_Mass_Rebuild 2026-07-16 20:49:58 +00:00
Lumir Balhar
9d73fb50fc Update to 26.1.2 (rhbz#2483638)
Co-Authored-By: Miro Hrončok <miro@hroncok.cz>
2026-07-03 11:23:30 +02:00
Python Maint
415ed181e5 Rebuilt for Python 3.15 2026-06-04 21:52:04 +02:00
Python Maint
3f4a4319ae Bump release 2026-06-03 13:59:55 +02:00
Python Maint
9583373c97 Bootstrap for Python 3.15 2026-06-03 13:47:16 +02:00
Miro Hrončok
c8ed4920c4 Never provide pip if this is not the build for the main Python
[skip changelog]
2026-05-23 22:17:15 +00:00
Lumir Balhar
5fd8a26f31 Remove Python 3.6 from CI configuration 2026-05-06 11:53:10 +02:00
Lumir Balhar
a5bfb663f2 Remove Python 3.9 from tests; pip no longer supports it since 26.1. 2026-05-06 11:53:10 +02:00
Lumir Balhar
3f3ab3fbad Update to 26.1.1 (rhbz#2466579) 2026-05-06 11:53:00 +02:00
10 changed files with 224 additions and 270 deletions

38
09a03f6cfa.patch Normal file
View 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",

View file

@ -1,30 +1,37 @@
From 6686b964762255a933e68939bb49710216ae7bb9 Mon Sep 17 00:00:00 2001 From 10dfb6b9005484578b386f64b9f36982e3dc6679 Mon Sep 17 00:00:00 2001
From: Damian Shaw <damian.peter.shaw@gmail.com> From: Damian Shaw <damian.peter.shaw@gmail.com>
Date: Fri, 14 Aug 2026 09:49:19 +0000 Date: Tue, 30 Jun 2026 21:52:39 -0400
Subject: [PATCH 2/2] Fix Link.filename double URL decode - path traversal Subject: [PATCH] Fix Link.filename decoding URL path twice (#14110)
(CVE-2026-13346)
Upstream PRs: https://github.com/pypa/pip/pull/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`.
- Add PathComponent newtype to enforce single-component filenames Drop the second decode, and add a `join_within_directory` helper so the
- Remove double urllib.parse.unquote() call in Link.filename download-path joins treat the name as a single path component.
- Add join_within_directory() preventing path escape at download sites
- Update download.py and prepare.py call sites
- Backport test coverage from upstream commits 1, 3, 4 of PR #14110
Co-Authored-By: Lumir Balhar <lbalhar@redhat.com>
--- ---
src/pip/_internal/models/link.py | 61 ++++++++++--- news/14110.bugfix.rst | 1 +
src/pip/_internal/models/link.py | 63 ++++++++++---
src/pip/_internal/network/download.py | 20 +++-- src/pip/_internal/network/download.py | 20 +++--
src/pip/_internal/operations/prepare.py | 6 +- src/pip/_internal/operations/prepare.py | 6 +-
tests/unit/test_link.py | 113 +++++++++++++++++++++++- tests/unit/test_link.py | 113 +++++++++++++++++++++++-
4 files changed, 179 insertions(+), 21 deletions(-) 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 diff --git a/src/pip/_internal/models/link.py b/src/pip/_internal/models/link.py
index 200ec34..e6a0c87 100644 index 0a09c66222..cbbe945c17 100644
--- a/src/pip/_internal/models/link.py --- a/src/pip/_internal/models/link.py
+++ b/src/pip/_internal/models/link.py +++ b/src/pip/_internal/models/link.py
@@ -14,6 +14,7 @@ from dataclasses import dataclass @@ -13,6 +13,7 @@
from typing import ( from typing import (
Any, Any,
NamedTuple, NamedTuple,
@ -32,7 +39,7 @@ index 200ec34..e6a0c87 100644
) )
from pip._internal.exceptions import InvalidEggFragment from pip._internal.exceptions import InvalidEggFragment
@@ -31,6 +32,47 @@ from pip._internal.utils.urls import path_to_url, url_to_path @@ -30,6 +31,49 @@
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -73,6 +80,8 @@ index 200ec34..e6a0c87 100644
+ +
+ ``component`` is a :data:`PathComponent`, so by type it has no separator and + ``component`` is a :data:`PathComponent`, so by type it has no separator and
+ is not a ``.`` or ``..`` reference; the result can never escape ``directory``. + 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) + return os.path.join(directory, component)
+ +
@ -80,7 +89,7 @@ index 200ec34..e6a0c87 100644
# Order matters, earlier hashes have a precedence over later hashes for what # Order matters, earlier hashes have a precedence over later hashes for what
# we will pick to use. # we will pick to use.
_SUPPORTED_HASHES = ("sha512", "sha384", "sha256", "sha224", "sha1", "md5") _SUPPORTED_HASHES = ("sha512", "sha384", "sha256", "sha224", "sha1", "md5")
@@ -423,18 +465,13 @@ class Link: @@ -424,18 +468,13 @@ def redacted_url(self) -> str:
return redact_auth_from_url(self.url) return redact_auth_from_url(self.url)
@property @property
@ -107,13 +116,13 @@ index 200ec34..e6a0c87 100644
@property @property
def file_path(self) -> str: def file_path(self) -> str:
diff --git a/src/pip/_internal/network/download.py b/src/pip/_internal/network/download.py diff --git a/src/pip/_internal/network/download.py b/src/pip/_internal/network/download.py
index 2696642..fa71c75 100644 index 039b268878..6faafb5cb0 100644
--- a/src/pip/_internal/network/download.py --- a/src/pip/_internal/network/download.py
+++ b/src/pip/_internal/network/download.py +++ b/src/pip/_internal/network/download.py
@@ -20,7 +20,12 @@ from pip._vendor.urllib3.exceptions import ReadTimeoutError @@ -19,7 +19,12 @@
from pip._internal.cli.progress_bars import BarType, get_download_progress_renderer from pip._internal.cli.progress_bars import BarType, get_download_progress_renderer
from pip._internal.exceptions import IncompleteDownloadError, NetworkConnectionError from pip._internal.exceptions import IncompleteDownloadError, NetworkConnectionError
from pip._internal.models.index import PyPI
-from pip._internal.models.link import Link -from pip._internal.models.link import Link
+from pip._internal.models.link import ( +from pip._internal.models.link import (
+ Link, + Link,
@ -124,7 +133,7 @@ index 2696642..fa71c75 100644
from pip._internal.network.cache import SafeFileCache, is_from_cache from pip._internal.network.cache import SafeFileCache, is_from_cache
from pip._internal.network.session import CacheControlAdapter, PipSession from pip._internal.network.session import CacheControlAdapter, PipSession
from pip._internal.network.utils import HEADERS, raise_for_status, response_chunks from pip._internal.network.utils import HEADERS, raise_for_status, response_chunks
@@ -117,11 +122,14 @@ def parse_content_disposition(content_disposition: str, default_filename: str) - @@ -121,11 +126,14 @@ def parse_content_disposition(content_disposition: str, default_filename: str) -
return filename or default_filename return filename or default_filename
@ -141,7 +150,7 @@ index 2696642..fa71c75 100644
# Have a look at the Content-Disposition header for a better guess # Have a look at the Content-Disposition header for a better guess
content_disposition = resp.headers.get("content-disposition") content_disposition = resp.headers.get("content-disposition")
if content_disposition: if content_disposition:
@@ -135,7 +143,7 @@ def _get_http_response_filename(resp: Response, link: Link) -> str: @@ -139,7 +147,7 @@ def _get_http_response_filename(resp: Response, link: Link) -> str:
ext = os.path.splitext(resp.url)[1] ext = os.path.splitext(resp.url)[1]
if ext: if ext:
filename += ext filename += ext
@ -150,7 +159,7 @@ index 2696642..fa71c75 100644
@dataclass @dataclass
@@ -188,7 +196,9 @@ class Downloader: @@ -192,7 +200,9 @@ def __call__(self, link: Link, location: str) -> tuple[str, str]:
resp = self._http_get(link) resp = self._http_get(link)
download_size = _get_http_response_size(resp) download_size = _get_http_response_size(resp)
@ -162,13 +171,13 @@ index 2696642..fa71c75 100644
download = _FileDownload(link, content_file, download_size) download = _FileDownload(link, content_file, download_size)
self._process_response(download, resp) self._process_response(download, resp)
diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py
index 67f9ee9..d260d15 100644 index afcc0376da..3b44403e0d 100644
--- a/src/pip/_internal/operations/prepare.py --- a/src/pip/_internal/operations/prepare.py
+++ b/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py
@@ -29,7 +29,7 @@ from pip._internal.exceptions import ( @@ -29,7 +29,7 @@
from pip._internal.index.package_finder import PackageFinder from pip._internal.index.package_finder import PackageFinder
from pip._internal.metadata import BaseDistribution, get_metadata_distribution from pip._internal.metadata import BaseDistribution, get_metadata_distribution
from pip._internal.models.direct_url import ArchiveInfo from pip._internal.models.direct_url import ArchiveInfo, DirectUrl
-from pip._internal.models.link import Link -from pip._internal.models.link import Link
+from pip._internal.models.link import Link, join_within_directory +from pip._internal.models.link import Link, join_within_directory
from pip._internal.models.wheel import Wheel from pip._internal.models.wheel import Wheel
@ -183,7 +192,7 @@ index 67f9ee9..d260d15 100644
if not os.path.exists(download_path): if not os.path.exists(download_path):
return None return None
@@ -683,7 +683,7 @@ class RequirementPreparer: @@ -687,7 +687,7 @@ def save_linked_requirement(self, req: InstallRequirement) -> None:
# No distribution was downloaded for this requirement. # No distribution was downloaded for this requirement.
return return
@ -193,7 +202,7 @@ index 67f9ee9..d260d15 100644
shutil.copy(req.local_file_path, download_location) shutil.copy(req.local_file_path, download_location)
download_path = display_path(download_location) download_path = display_path(download_location)
diff --git a/tests/unit/test_link.py b/tests/unit/test_link.py diff --git a/tests/unit/test_link.py b/tests/unit/test_link.py
index c49f854..bc8cb8a 100644 index c49f8547ac..bc8cb8ab9b 100644
--- a/tests/unit/test_link.py --- a/tests/unit/test_link.py
+++ b/tests/unit/test_link.py +++ b/tests/unit/test_link.py
@@ -1,9 +1,17 @@ @@ -1,9 +1,17 @@
@ -215,7 +224,7 @@ index c49f854..bc8cb8a 100644
from pip._internal.utils.hashes import Hashes from pip._internal.utils.hashes import Hashes
@@ -29,6 +37,13 @@ class TestLink: @@ -29,6 +37,13 @@ def test_repr(self, url: str, expected: str) -> None:
("https://example.com/path/page.html", "page.html"), ("https://example.com/path/page.html", "page.html"),
# Test a quoted character. # Test a quoted character.
("https://example.com/path/page%231.html", "page#1.html"), ("https://example.com/path/page%231.html", "page#1.html"),
@ -229,7 +238,7 @@ index c49f854..bc8cb8a 100644
( (
"http://yo/myproject-1.0%2Bfoobar.0-py2.py3-none-any.whl", "http://yo/myproject-1.0%2Bfoobar.0-py2.py3-none-any.whl",
"myproject-1.0+foobar.0-py2.py3-none-any.whl", "myproject-1.0+foobar.0-py2.py3-none-any.whl",
@@ -49,6 +64,52 @@ class TestLink: @@ -49,6 +64,52 @@ def test_filename(self, url: str, expected: str) -> None:
link = Link(url) link = Link(url)
assert link.filename == expected assert link.filename == expected
@ -336,6 +345,3 @@ index c49f854..bc8cb8a 100644
+ joined = join_within_directory(directory, as_path_component(name)) + joined = join_within_directory(directory, as_path_component(name))
+ assert joined == os.path.join(directory, name) + assert joined == os.path.join(directory, name)
+ assert os.path.basename(joined) == name + assert os.path.basename(joined) == name
--
2.55.0

29
4c6d7471de.patch Normal file
View 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
View 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

View file

@ -1,139 +0,0 @@
From c840c212830bb8e9dab56de6c57916b111858aec Mon Sep 17 00:00:00 2001
From: Damian Shaw <damian.peter.shaw@gmail.com>
Date: Mon, 18 May 2026 23:04:43 -0400
Subject: [PATCH 1/2] Reject entry point names that escape scripts dir
---
src/pip/_internal/operations/install/wheel.py | 26 +++++++-
tests/unit/test_wheel.py | 64 +++++++++++++++++++
2 files changed, 87 insertions(+), 3 deletions(-)
diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py
index 40097d6..231e400 100644
--- a/src/pip/_internal/operations/install/wheel.py
+++ b/src/pip/_internal/operations/install/wheel.py
@@ -397,11 +397,31 @@ class MissingCallableSuffix(InstallationError):
)
-def _raise_for_invalid_entrypoint(specification: str) -> None:
+def _script_within_dir(name: str, scripts_dir: str) -> bool:
+ """Return whether script ``name`` resolves to a path inside the ``scripts_dir``.
+
+ distlib joins the entry point name onto the scripts directory, so a name
+ with path separators or ``..`` components can resolve elsewhere.
+ """
+ root = os.path.normpath(scripts_dir)
+ dest = os.path.normpath(os.path.join(scripts_dir, name))
+ return dest.startswith(root + os.sep)
+
+
+def _raise_for_invalid_entrypoint(specification: str, scripts_dir: str) -> None:
entry = get_export_entry(specification)
- if entry is not None and entry.suffix is None:
+ if entry is None:
+ return
+
+ if entry.suffix is None:
raise MissingCallableSuffix(str(entry))
+ if not _script_within_dir(entry.name, scripts_dir):
+ raise InstallationError(
+ f"Invalid script entry point name {entry.name!r}: the script "
+ f"would be installed outside the scripts directory ({scripts_dir})."
+ )
+
class PipScriptMaker(ScriptMaker):
# Override distlib's default script template with one that
@@ -419,7 +439,7 @@ class PipScriptMaker(ScriptMaker):
def make(
self, specification: str, options: dict[str, Any] | None = None
) -> list[str]:
- _raise_for_invalid_entrypoint(specification)
+ _raise_for_invalid_entrypoint(specification, self.target_dir)
return super().make(specification, options)
diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py
index e0ac649..c1aafe5 100644
--- a/tests/unit/test_wheel.py
+++ b/tests/unit/test_wheel.py
@@ -462,6 +462,32 @@ class TestInstallUnpackedWheel:
assert os.path.basename(wheel_path) in exc_text
assert entrypoint in exc_text
+ @pytest.mark.parametrize("bad_name", ["../../outside", "..", "."])
+ @pytest.mark.parametrize("entry_point_type", ["console_scripts", "gui_scripts"])
+ def test_wheel_install_rejects_entry_point_path_traversal(
+ self, data: TestData, tmpdir: Path, bad_name: str, entry_point_type: str
+ ) -> None:
+ """An entry point name with separators or ``..`` must not install a
+ script outside the scripts directory.
+ """
+ self.prep(data, tmpdir)
+ wheel_path = make_wheel(
+ "simple",
+ "0.1.0",
+ entry_points={entry_point_type: [f"{bad_name} = simple:main"]},
+ ).save_to_dir(tmpdir)
+ with pytest.raises(InstallationError) as e:
+ wheel.install_wheel(
+ "simple",
+ str(wheel_path),
+ scheme=self.scheme,
+ req_description="simple",
+ )
+
+ assert "outside the scripts directory" in str(e.value)
+ # Nothing was written outside the install destination.
+ assert not os.path.exists(os.path.join(str(tmpdir), "outside"))
+
class TestMessageAboutScriptsNotOnPATH:
tilde_warning_msg = (
@@ -665,3 +691,41 @@ def test_get_console_script_specs_replaces_python_version(
"not_pip_or_easy_install-99 = whatever",
"not_pip_or_easy_install-99.88 = whatever",
]
+
+
+@pytest.mark.parametrize(
+ "name, within",
+ [
+ ("pip", True),
+ ("pip3.13", True),
+ ("foo-bar.baz", True),
+ ("...", True), # a literal filename, not a path component
+ ("sub/script", True), # in-tree subdirectory
+ ("a/../b", True),
+ ("sub\\script", True), # backslash stays in-tree on POSIX and Windows
+ (" ../../inside", True), # distlib keeps a leading space; resolves in-tree
+ ("../outside", False),
+ ("../../outside", False),
+ ("a/../../outside", False),
+ ("/etc/cron.d/outside", False), # absolute path; os.path.join drops the root
+ # "." and ".." pass PyPI's [\w.-]+ name check but must be rejected here.
+ (".", False),
+ ("..", False),
+ ("", False),
+ ],
+)
+def test_script_within_dir(name: str, within: bool) -> None:
+ assert wheel._script_within_dir(name, "/srv/env/bin") is within
+
+
+def test_script_within_dir_allows_doubled_slash_root() -> None:
+ # A scripts directory can have a doubled leading slash
+ assert wheel._script_within_dir("pip", "//srv/env/bin") is True
+ assert wheel._script_within_dir("../outside", "//srv/env/bin") is False
+
+
+@pytest.mark.skipif(not WINDOWS, reason="drive letters only matter on Windows")
+def test_script_within_dir_rejects_other_drive() -> None:
+ # Validate that a script on a different drive is rejected,
+ # and doesn't throw an error
+ assert wheel._script_within_dir("D:\\outside", "C:\\env\\bin") is False
--
2.55.0

View file

@ -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

View file

@ -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,18 +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
# CVE-2026-8643: entry point path traversal in console_scripts/gui_scripts # Fix user-site path ordering in the test suite on Python 3.15+
# Upstream fix: https://github.com/pypa/pip/pull/14000 # The same CPython gh-149819 change that broke build env isolation also broke
Patch: CVE-2026-8643.patch # _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 # CVE-2026-13346: Link.filename double URL decode allows path traversal
# Upstream fix: https://github.com/pypa/pip/pull/14110 Patch: https://github.com/pypa/pip/commit/10dfb6b900.patch
Patch: CVE-2026-13346.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
@ -126,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.
@ -185,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
@ -299,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}}
@ -322,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

View file

@ -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

View file

@ -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

View file

@ -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