From fc0f7c683c372d66f2e2d6edc00909cc5f225f67 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 CVE: CVE-2026-8643 Upstream-Status: Backport [https://github.com/pypa/pip/commit/8eb178480bd1a2b223f509fc430796b265158dfb] Backport Changes: - Omit tests/unit/test_wheel.py because the pip 24.0 PyPI sdist used by this recipe does not ship the upstream tests directory. (cherry picked from commit 8eb178480bd1a2b223f509fc430796b265158dfb) Signed-off-by: Hetvi Thakar --- 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 f67180c9e..8a36a66ae 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -409,17 +409,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) -- 2.35.6