Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 149 additions & 0 deletions SPECS/python-pip/CVE-2026-8643.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
From 56a7c7351b378965cec359059a5c29db07e6b30a Mon Sep 17 00:00:00 2001
From: AllSpark <allspark@microsoft.com>
Date: Wed, 3 Jun 2026 11:05:39 +0000
Subject: [PATCH] Reject entry point names that escape scripts dir

Signed-off-by: Azure Linux Security Servicing Account <azurelinux-security@microsoft.com>
Upstream-reference: AI Backport of https://github.com/pypa/pip/pull/14000.patch
---
news/14000.bugfix.rst | 2 ++
src/pip/_internal/operations/install/wheel.py | 26 +++++++-
tests/unit/test_wheel.py | 64 +++++++++++++++++++
3 files changed, 88 insertions(+), 3 deletions(-)
create mode 100644 news/14000.bugfix.rst

diff --git a/news/14000.bugfix.rst b/news/14000.bugfix.rst
new file mode 100644
index 0000000..36df64b
--- /dev/null
+++ b/news/14000.bugfix.rst
@@ -0,0 +1,2 @@
+Reject ``console_scripts`` and ``gui_scripts`` entry points whose name would
+install a script outside the scripts directory.
diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py
index aef42aa..84ae542 100644
--- a/src/pip/_internal/operations/install/wheel.py
+++ b/src/pip/_internal/operations/install/wheel.py
@@ -405,17 +405,37 @@ 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):
def make(
self, specification: str, options: Optional[Dict[str, Any]] = 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 ed6f582..5350b6d 100644
--- a/tests/unit/test_wheel.py
+++ b/tests/unit/test_wheel.py
@@ -515,6 +515,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 = (
@@ -718,3 +744,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.45.4

6 changes: 5 additions & 1 deletion SPECS/python-pip/python-pip.spec
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ A tool for installing and managing Python packages}
Summary: A tool for installing and managing Python packages
Name: python-pip
Version: 24.2
Release: 8%{?dist}
Release: 9%{?dist}
License: MIT AND Python-2.0.1 AND Apache-2.0 AND BSD-2-Clause AND BSD-3-Clause AND ISC AND LGPL-2.1-only AND MPL-2.0 AND (Apache-2.0 OR BSD-2-Clause)
Vendor: Microsoft Corporation
Distribution: Azure Linux
Expand All @@ -18,6 +18,7 @@ Patch2: CVE-2025-50181.patch
Patch3: CVE-2026-1703.patch
Patch4: CVE-2026-3219.patch
Patch5: CVE-2026-6357.patch
Patch6: CVE-2026-8643.patch

BuildArch: noarch

Expand Down Expand Up @@ -61,6 +62,9 @@ BuildRequires: python3-wheel
%{python3_sitelib}/pip*

%changelog
* Wed Jun 03 2026 Azure Linux Security Servicing Account <azurelinux-security@microsoft.com> - 24.2-9
- Patch for CVE-2026-8643

* Fri May 08 2026 Azure Linux Security Servicing Account <azurelinux-security@microsoft.com> - 24.2-8
- Patch for CVE-2026-6357

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -550,7 +550,7 @@ python3-magic-5.45-1.azl3.noarch.rpm
python3-markupsafe-2.1.3-1.azl3.aarch64.rpm
python3-newt-0.52.23-1.azl3.aarch64.rpm
python3-packaging-23.2-3.azl3.noarch.rpm
python3-pip-24.2-8.azl3.noarch.rpm
python3-pip-24.2-9.azl3.noarch.rpm
python3-pygments-2.7.4-2.azl3.noarch.rpm
python3-rpm-4.18.2-1.azl3.aarch64.rpm
python3-rpm-generators-14-11.azl3.noarch.rpm
Expand Down
2 changes: 1 addition & 1 deletion toolkit/resources/manifests/package/toolchain_x86_64.txt
Original file line number Diff line number Diff line change
Expand Up @@ -558,7 +558,7 @@ python3-magic-5.45-1.azl3.noarch.rpm
python3-markupsafe-2.1.3-1.azl3.x86_64.rpm
python3-newt-0.52.23-1.azl3.x86_64.rpm
python3-packaging-23.2-3.azl3.noarch.rpm
python3-pip-24.2-8.azl3.noarch.rpm
python3-pip-24.2-9.azl3.noarch.rpm
python3-pygments-2.7.4-2.azl3.noarch.rpm
python3-rpm-4.18.2-1.azl3.x86_64.rpm
python3-rpm-generators-14-11.azl3.noarch.rpm
Expand Down
Loading