|
| 1 | +From ed59537ac3709cfbdbf95d957de801c13872991a Mon Sep 17 00:00:00 2001 |
| 2 | +From: =?UTF-8?q?Randy=20D=C3=B6ring?= |
| 3 | + <30527984+radoering@users.noreply.github.com> |
| 4 | +Date: Sun, 29 Mar 2026 10:24:17 +0200 |
| 5 | +Subject: [PATCH] installer: fix path traversal (#10792) |
| 6 | + |
| 7 | +Upstream Patch Reference: https://github.com/python-poetry/poetry/commit/ed59537ac3709cfbdbf95d957de801c13872991a.patch |
| 8 | +--- |
| 9 | + src/poetry/installation/wheel_installer.py | 9 +++- |
| 10 | + tests/installation/test_wheel_installer.py | 48 ++++++++++++++++++++++ |
| 11 | + 2 files changed, 56 insertions(+), 1 deletion(-) |
| 12 | + |
| 13 | +diff --git a/src/poetry/installation/wheel_installer.py b/src/poetry/installation/wheel_installer.py |
| 14 | +index 27a867f..d3defb4 100644 |
| 15 | +--- a/src/poetry/installation/wheel_installer.py |
| 16 | ++++ b/src/poetry/installation/wheel_installer.py |
| 17 | +@@ -44,7 +44,14 @@ class WheelDestination(SchemeDictionaryDestination): |
| 18 | + from installer.utils import copyfileobj_with_hashing |
| 19 | + from installer.utils import make_file_executable |
| 20 | + |
| 21 | +- target_path = Path(self.scheme_dict[scheme]) / path |
| 22 | ++ target_dir = Path(self.scheme_dict[scheme]).resolve() |
| 23 | ++ target_path = (target_dir / path).resolve() |
| 24 | ++ |
| 25 | ++ if not target_path.is_relative_to(target_dir): |
| 26 | ++ raise ValueError( |
| 27 | ++ f"Attempting to write {path} outside of the target directory" |
| 28 | ++ ) |
| 29 | ++ |
| 30 | + if target_path.exists(): |
| 31 | + # Contrary to the base library we don't raise an error here since it can |
| 32 | + # break pkgutil-style and pkg_resource-style namespace packages. |
| 33 | +diff --git a/tests/installation/test_wheel_installer.py b/tests/installation/test_wheel_installer.py |
| 34 | +index b7b3d7c..f891b3c 100644 |
| 35 | +--- a/tests/installation/test_wheel_installer.py |
| 36 | ++++ b/tests/installation/test_wheel_installer.py |
| 37 | +@@ -81,3 +81,51 @@ def test_enable_bytecode_compilation( |
| 38 | + assert not list(cache_dir.glob("*.opt-2.pyc")) |
| 39 | + else: |
| 40 | + assert not cache_dir.exists() |
| 41 | ++ |
| 42 | ++ |
| 43 | ++def test_install_dir_is_symlink(tmp_path: Path, demo_wheel: Path) -> None: |
| 44 | ++ target_dir = tmp_path / "target" |
| 45 | ++ target_dir.mkdir() |
| 46 | ++ symlink_dir = tmp_path / "symlink" |
| 47 | ++ symlink_dir.symlink_to(target_dir, target_is_directory=True) |
| 48 | ++ |
| 49 | ++ env = MockEnv(path=symlink_dir) |
| 50 | ++ |
| 51 | ++ installer = WheelInstaller(env) |
| 52 | ++ installer.install(demo_wheel) |
| 53 | ++ |
| 54 | ++ assert (Path(env.paths["purelib"]) / "demo").exists() |
| 55 | ++ |
| 56 | ++ |
| 57 | ++@pytest.fixture |
| 58 | ++def wheel_with_path_traversal(tmp_path: Path) -> Path: |
| 59 | ++ import zipfile |
| 60 | ++ |
| 61 | ++ wheel = tmp_path / "traversal-0.1-py3-none-any.whl" |
| 62 | ++ files = { |
| 63 | ++ "traversal/__init__.py": b"", |
| 64 | ++ "../../traversal.txt": b"", |
| 65 | ++ "traversal-0.1.dist-info/WHEEL": ( |
| 66 | ++ b"Wheel-Version: 1.0\nRoot-Is-Purelib: true\nTag: py3-none-any\n" |
| 67 | ++ ), |
| 68 | ++ "traversal-0.1.dist-info/METADATA": ( |
| 69 | ++ b"Metadata-Version: 2.1\nName: traversal\nVersion: 0.1\n" |
| 70 | ++ ), |
| 71 | ++ } |
| 72 | ++ files["traversal-0.1.dist-info/RECORD"] = ( |
| 73 | ++ "\n".join([f"{k},," for k in files] + ["traversal-0.1.dist-info/RECORD,,"]) |
| 74 | ++ + "\n" |
| 75 | ++ ).encode() |
| 76 | ++ |
| 77 | ++ with zipfile.ZipFile(wheel, "w") as z: |
| 78 | ++ for k, v in files.items(): |
| 79 | ++ z.writestr(k, v) |
| 80 | ++ |
| 81 | ++ return wheel |
| 82 | ++ |
| 83 | ++ |
| 84 | ++def test_path_traversal(env: MockEnv, wheel_with_path_traversal: Path) -> None: |
| 85 | ++ installer = WheelInstaller(env) |
| 86 | ++ with pytest.raises(ValueError): |
| 87 | ++ installer.install(wheel_with_path_traversal) |
| 88 | ++ assert not (env.path.parent / "traversal.txt").exists() |
| 89 | +-- |
| 90 | +2.45.4 |
| 91 | + |
0 commit comments