From 483d83c13c9d69c1916c06cab29991f6c2725cee Mon Sep 17 00:00:00 2001
From: Damian Shaw <damian.peter.shaw@gmail.com>
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

CVE: CVE-2026-8643
Upstream-Status: Backport [https://github.com/pypa/pip/commit/8eb178480bd1a2b223f509fc430796b265158dfb]

Backport Changes:
- Omitted tests/unit/test_wheel.py because the pip 26.0.1 PyPI sdist
  does not ship the upstream test suite and the OE recipe does not
  enable ptest.

(cherry picked from commit 8eb178480bd1a2b223f509fc430796b265158dfb)
Signed-off-by: Hetvi Thakar <hthakar@cisco.com>
---
 news/14000.bugfix.rst                         |  2 ++
 src/pip/_internal/operations/install/wheel.py | 26 ++++++++++++++++---
 2 files changed, 25 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 000000000..3b86f1b3b
--- /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 40097d6a7..231e40065 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)
 
 
