diff --git a/docs/changelog/206.security.rst b/docs/changelog/206.security.rst new file mode 100644 index 0000000..ec8c3f5 --- /dev/null +++ b/docs/changelog/206.security.rst @@ -0,0 +1,2 @@ +When imported from an archive, python-discovery now extracts its interpreter probe into a private temporary directory. +This prevents the probe from importing modules supplied in the shared temp directory. diff --git a/src/python_discovery/_cached_py_info.py b/src/python_discovery/_cached_py_info.py index 0a28cf3..d9aff86 100644 --- a/src/python_discovery/_cached_py_info.py +++ b/src/python_discovery/_cached_py_info.py @@ -195,13 +195,11 @@ def _resolve_py_info_script() -> Generator[Path]: if data is None: msg = f"cannot locate {_PY_INFO_SCRIPT.name} for subprocess interrogation" raise FileNotFoundError(msg) - fd, tmp = tempfile.mkstemp(suffix=".py") - try: - os.write(fd, data) - os.close(fd) - yield Path(tmp) - finally: - Path(tmp).unlink() + # Python prepends the probe script's directory to sys.path, so keep that directory private. + with tempfile.TemporaryDirectory() as private_dir: + script: Final[Path] = Path(private_dir) / _PY_INFO_SCRIPT.name + script.write_bytes(data) + yield script def _extract_between_cookies(out: str, start_cookie: str, end_cookie: str) -> tuple[str, str, int, int]: diff --git a/tests/test_cached_py_info.py b/tests/test_cached_py_info.py index feac837..04bd5ba 100644 --- a/tests/test_cached_py_info.py +++ b/tests/test_cached_py_info.py @@ -4,6 +4,7 @@ import logging import os import sys +import tempfile from pathlib import Path from subprocess import TimeoutExpired, list2cmdline from typing import TYPE_CHECKING, Final @@ -59,7 +60,9 @@ def test_resolve_py_info_script_fallback_to_pkgutil(mocker: MockerFixture) -> No assert script.exists() content = script.read_text(encoding="utf-8") assert content == "# mock script" - assert not script.exists() + assert script.parent != Path(tempfile.gettempdir()) + assert list(script.parent.iterdir()) == [script] + assert not script.parent.exists() def test_resolve_py_info_script_pkgutil_returns_none(mocker: MockerFixture) -> None: