diff --git a/CVE-2026-8643-Reject-entry-point-names-that-escape-scripts-dir-140.patch b/CVE-2026-8643-Reject-entry-point-names-that-escape-scripts-dir-140.patch new file mode 100644 index 0000000000000000000000000000000000000000..dfb52bbb5c2f5ae65989e39b0b4cbe371b7c7caa --- /dev/null +++ b/CVE-2026-8643-Reject-entry-point-names-that-escape-scripts-dir-140.patch @@ -0,0 +1,152 @@ +From 8eb178480bd1a2b223f509fc430796b265158dfb Mon Sep 17 00:00:00 2001 +From: Damian Shaw +Date: Wed, 20 May 2026 15:20:25 -0400 +Subject: [PATCH] Reject entry point names that escape scripts dir (#14000) + +* Reject entry point names that escape scripts dir + +* NEWS ENTRY +--- + news/14000.bugfix.rst | 2 + + src/pip/_internal/operations/install/wheel.py | 28 +++++++- + tests/unit/test_wheel.py | 64 +++++++++++++++++++ + 3 files changed, 91 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..3b86f1b +--- /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 58a7730..000d43a 100644 +--- a/src/pip/_internal/operations/install/wheel.py ++++ b/src/pip/_internal/operations/install/wheel.py +@@ -415,17 +415,39 @@ 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 ``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( ++ "Invalid script entry point name {!r}: the script would be " ++ "installed outside the scripts directory ({}).".format( ++ entry.name, 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 6d6d1a3..6e336d3 100644 +--- a/tests/unit/test_wheel.py ++++ b/tests/unit/test_wheel.py +@@ -516,6 +516,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: ["{} = simple:main".format(bad_name)]}, ++ ).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 = ( +@@ -719,3 +745,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), ++ # "." 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.39.3 + diff --git a/python-pip.spec b/python-pip.spec index 4e6a87e1efcb68f2484f68c6b78bf6e0363dbaeb..06ddeae541b2f4aeff255ccbf3041543ad1ca4ce 100644 --- a/python-pip.spec +++ b/python-pip.spec @@ -14,12 +14,13 @@ Summary: The Python package installer Name: python-%{srcname} Version: 23.3.1 -Release: 6%{?dist} +Release: 7%{?dist} License: MIT and Python and ASL 2.0 and BSD and ISC and LGPLv2 and MPLv2.0 and (ASL 2.0 or BSD) URL: https://pip.pypa.io/ Source0: https://github.com/pypa/pip/archive/%{version}/%{srcname}-%{version}.tar.gz Patch0001: CVE-2026-1703-Use-os.path.commonpath-instead-of-commonprefix.patch +Patch0002: CVE-2026-8643-Reject-entry-point-names-that-escape-scripts-dir-140.patch Patch3001: remove-existing-dist-only-if-path-conflicts.patch Patch3002: dummy-certifi.patch @@ -227,6 +228,10 @@ rm -f tests/conftest.py %{python_wheel_dir}/%{srcname}-%{version}-py3-none-any.whl %changelog +* Fri May 29 2026 Shuo Wang - 23.3.1-7 +- [Type] security +- [DESC] fix CVE-2026-8643, Reject entry point names that escape scripts dir (#14000) + * Thu Feb 5 2026 Shuo Wang - 23.3.1-6 - fix CVE-2026-1703 - Use os.path.commonpath() instead of commonprefix()