Version in base suite: 2.1.2+dfsg-1 Base version: poetry_2.1.2+dfsg-1 Target version: poetry_2.1.2+dfsg-1+deb13u1 Base file: /srv/ftp-master.debian.org/ftp/pool/main/p/poetry/poetry_2.1.2+dfsg-1.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/p/poetry/poetry_2.1.2+dfsg-1+deb13u1.dsc changelog | 8 patches/0001-installer-fix-path-traversal-10792.patch | 90 ++++ patches/0002-perf-use-os.path.abspath-instead-of-Path.resolve-108.patch | 185 ++++++++++ patches/series | 2 4 files changed, 285 insertions(+) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpn83_hiui/poetry_2.1.2+dfsg-1.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpn83_hiui/poetry_2.1.2+dfsg-1+deb13u1.dsc: no acceptable signature found diff -Nru poetry-2.1.2+dfsg/debian/changelog poetry-2.1.2+dfsg/debian/changelog --- poetry-2.1.2+dfsg/debian/changelog 2025-04-03 23:49:58.000000000 +0000 +++ poetry-2.1.2+dfsg/debian/changelog 2026-07-03 15:25:04.000000000 +0000 @@ -1,3 +1,11 @@ +poetry (2.1.2+dfsg-1+deb13u1) trixie; urgency=medium + + * Non-maintainer upload. + * CVE-2026-34591: Wheel Path Traversal Leading to Arbitrary File Write + (Closes: #1132609) + + -- Adrian Bunk Fri, 03 Jul 2026 18:25:04 +0300 + poetry (2.1.2+dfsg-1) unstable; urgency=medium * New upstream version. diff -Nru poetry-2.1.2+dfsg/debian/patches/0001-installer-fix-path-traversal-10792.patch poetry-2.1.2+dfsg/debian/patches/0001-installer-fix-path-traversal-10792.patch --- poetry-2.1.2+dfsg/debian/patches/0001-installer-fix-path-traversal-10792.patch 1970-01-01 00:00:00.000000000 +0000 +++ poetry-2.1.2+dfsg/debian/patches/0001-installer-fix-path-traversal-10792.patch 2026-07-03 15:24:45.000000000 +0000 @@ -0,0 +1,90 @@ +From 6895943e9e2f9ce563ea51b984830fb87f4d7371 Mon Sep 17 00:00:00 2001 +From: Randy Döring <30527984+radoering@users.noreply.github.com> +Date: Sun, 29 Mar 2026 10:24:17 +0200 +Subject: installer: fix path traversal (#10792) + +(cherry picked from commit ed59537ac3709cfbdbf95d957de801c13872991a) +--- + src/poetry/installation/wheel_installer.py | 9 +++- + tests/installation/test_wheel_installer.py | 48 ++++++++++++++++++++++ + 2 files changed, 56 insertions(+), 1 deletion(-) + +diff --git a/src/poetry/installation/wheel_installer.py b/src/poetry/installation/wheel_installer.py +index 57ee02d9..a086ca73 100644 +--- a/src/poetry/installation/wheel_installer.py ++++ b/src/poetry/installation/wheel_installer.py +@@ -44,7 +44,14 @@ class WheelDestination(SchemeDictionaryDestination): + from installer.utils import copyfileobj_with_hashing + from installer.utils import make_file_executable + +- target_path = Path(self.scheme_dict[scheme]) / path ++ target_dir = Path(self.scheme_dict[scheme]).resolve() ++ target_path = (target_dir / path).resolve() ++ ++ if not target_path.is_relative_to(target_dir): ++ raise ValueError( ++ f"Attempting to write {path} outside of the target directory" ++ ) ++ + if target_path.exists(): + # Contrary to the base library we don't raise an error here since it can + # break pkgutil-style and pkg_resource-style namespace packages. +diff --git a/tests/installation/test_wheel_installer.py b/tests/installation/test_wheel_installer.py +index 98e3f4cf..33e95dd9 100644 +--- a/tests/installation/test_wheel_installer.py ++++ b/tests/installation/test_wheel_installer.py +@@ -81,3 +81,51 @@ def test_enable_bytecode_compilation( + assert not list(cache_dir.glob("*.opt-2.pyc")) + else: + assert not cache_dir.exists() ++ ++ ++def test_install_dir_is_symlink(tmp_path: Path, demo_wheel: Path) -> None: ++ target_dir = tmp_path / "target" ++ target_dir.mkdir() ++ symlink_dir = tmp_path / "symlink" ++ symlink_dir.symlink_to(target_dir, target_is_directory=True) ++ ++ env = MockEnv(path=symlink_dir) ++ ++ installer = WheelInstaller(env) ++ installer.install(demo_wheel) ++ ++ assert (Path(env.paths["purelib"]) / "demo").exists() ++ ++ ++@pytest.fixture ++def wheel_with_path_traversal(tmp_path: Path) -> Path: ++ import zipfile ++ ++ wheel = tmp_path / "traversal-0.1-py3-none-any.whl" ++ files = { ++ "traversal/__init__.py": b"", ++ "../../traversal.txt": b"", ++ "traversal-0.1.dist-info/WHEEL": ( ++ b"Wheel-Version: 1.0\nRoot-Is-Purelib: true\nTag: py3-none-any\n" ++ ), ++ "traversal-0.1.dist-info/METADATA": ( ++ b"Metadata-Version: 2.1\nName: traversal\nVersion: 0.1\n" ++ ), ++ } ++ files["traversal-0.1.dist-info/RECORD"] = ( ++ "\n".join([f"{k},," for k in files] + ["traversal-0.1.dist-info/RECORD,,"]) ++ + "\n" ++ ).encode() ++ ++ with zipfile.ZipFile(wheel, "w") as z: ++ for k, v in files.items(): ++ z.writestr(k, v) ++ ++ return wheel ++ ++ ++def test_path_traversal(env: MockEnv, wheel_with_path_traversal: Path) -> None: ++ installer = WheelInstaller(env) ++ with pytest.raises(ValueError): ++ installer.install(wheel_with_path_traversal) ++ assert not (env.path.parent / "traversal.txt").exists() +-- +2.47.3 + diff -Nru poetry-2.1.2+dfsg/debian/patches/0002-perf-use-os.path.abspath-instead-of-Path.resolve-108.patch poetry-2.1.2+dfsg/debian/patches/0002-perf-use-os.path.abspath-instead-of-Path.resolve-108.patch --- poetry-2.1.2+dfsg/debian/patches/0002-perf-use-os.path.abspath-instead-of-Path.resolve-108.patch 1970-01-01 00:00:00.000000000 +0000 +++ poetry-2.1.2+dfsg/debian/patches/0002-perf-use-os.path.abspath-instead-of-Path.resolve-108.patch 2026-07-03 15:24:45.000000000 +0000 @@ -0,0 +1,185 @@ +From bd6f54aec6e4f07dcf77ad9e8acee474e55cb06a Mon Sep 17 00:00:00 2001 +From: Randy Döring <30527984+radoering@users.noreply.github.com> +Date: Sun, 12 Apr 2026 14:21:51 +0200 +Subject: perf: use `os.path.abspath()` instead of `Path.resolve()` (#10821) + +(cherry picked from commit a029a41ea4adb1b35e9d1ba4fe391cd1c77804d8) +--- + src/poetry/installation/wheel_installer.py | 15 ++- + tests/installation/test_wheel_installer.py | 101 +++++++++++++++++++-- + 2 files changed, 108 insertions(+), 8 deletions(-) + +diff --git a/src/poetry/installation/wheel_installer.py b/src/poetry/installation/wheel_installer.py +index a086ca73..c4fff3b1 100644 +--- a/src/poetry/installation/wheel_installer.py ++++ b/src/poetry/installation/wheel_installer.py +@@ -1,6 +1,7 @@ + from __future__ import annotations + + import logging ++import os + import platform + import sys + +@@ -44,8 +45,18 @@ class WheelDestination(SchemeDictionaryDestination): + from installer.utils import copyfileobj_with_hashing + from installer.utils import make_file_executable + +- target_dir = Path(self.scheme_dict[scheme]).resolve() +- target_path = (target_dir / path).resolve() ++ # See https://docs.python.org/3/library/zipfile.html#zipfile.Path: ++ # When handling untrusted archives, ++ # consider resolving filenames using os.path.abspath() ++ # and checking against the target directory with os.path.commonpath(). ++ # ++ # Attention: Path.absolute() is not sufficient because it does not ++ # normalize, i.e. does not remove "..". ++ # ++ # We want to avoid Path.resolve() because it is significantly slower ++ # than os.path.abspath()! ++ target_dir = Path(os.path.abspath(self.scheme_dict[scheme])) ++ target_path = Path(os.path.abspath(target_dir / path)) + + if not target_path.is_relative_to(target_dir): + raise ValueError( +diff --git a/tests/installation/test_wheel_installer.py b/tests/installation/test_wheel_installer.py +index 33e95dd9..3ab60a59 100644 +--- a/tests/installation/test_wheel_installer.py ++++ b/tests/installation/test_wheel_installer.py +@@ -10,6 +10,7 @@ import pytest + from poetry.core.constraints.version import parse_constraint + + from poetry.installation.wheel_installer import WheelInstaller ++from poetry.utils._compat import WINDOWS + from poetry.utils.env import MockEnv + + +@@ -21,7 +22,7 @@ if TYPE_CHECKING: + + @pytest.fixture + def env(tmp_path: Path) -> MockEnv: +- return MockEnv(path=tmp_path) ++ return MockEnv(path=tmp_path / "env") + + + @pytest.fixture(scope="module") +@@ -97,14 +98,20 @@ def test_install_dir_is_symlink(tmp_path: Path, demo_wheel: Path) -> None: + assert (Path(env.paths["purelib"]) / "demo").exists() + + +-@pytest.fixture +-def wheel_with_path_traversal(tmp_path: Path) -> Path: ++@pytest.fixture(params=[False, True]) # relative path ++def wheel_with_path_traversal(tmp_path: Path, request: pytest.FixtureRequest) -> Path: + import zipfile + ++ traversal_path = ( ++ "../../traversal.txt" ++ if request.param ++ else (tmp_path / "traversal.txt").as_posix() ++ ) ++ + wheel = tmp_path / "traversal-0.1-py3-none-any.whl" + files = { + "traversal/__init__.py": b"", +- "../../traversal.txt": b"", ++ traversal_path: b"path traversal", + "traversal-0.1.dist-info/WHEEL": ( + b"Wheel-Version: 1.0\nRoot-Is-Purelib: true\nTag: py3-none-any\n" + ), +@@ -124,8 +131,90 @@ def wheel_with_path_traversal(tmp_path: Path) -> Path: + return wheel + + +-def test_path_traversal(env: MockEnv, wheel_with_path_traversal: Path) -> None: ++@pytest.mark.parametrize("existing", [False, True]) ++def test_no_path_traversal( ++ env: MockEnv, wheel_with_path_traversal: Path, existing: bool ++) -> None: ++ target = env.path.parent / "traversal.txt" ++ if existing: ++ target.write_text("original", encoding="utf-8") + installer = WheelInstaller(env) + with pytest.raises(ValueError): + installer.install(wheel_with_path_traversal) +- assert not (env.path.parent / "traversal.txt").exists() ++ ++ if existing: ++ assert target.exists() ++ assert target.read_text(encoding="utf-8") == "original" ++ else: ++ assert not target.exists() ++ ++ ++@pytest.fixture(params=[False, True]) # relative path ++def wheel_with_symlink(tmp_path: Path, request: pytest.FixtureRequest) -> Path: ++ import stat ++ import zipfile ++ ++ wheel = tmp_path / "symlink-0.1-py3-none-any.whl" ++ files = { ++ "symlink/__init__.py": b"", ++ "symlink-0.1.dist-info/WHEEL": ( ++ b"Wheel-Version: 1.0\nRoot-Is-Purelib: true\nTag: py3-none-any\n" ++ ), ++ "symlink-0.1.dist-info/METADATA": ( ++ b"Metadata-Version: 2.1\nName: symlink-pkg\nVersion: 0.1\n" ++ ), ++ } ++ ++ symlink_entry = "symlink/traversal_link" ++ symlink_target = ( ++ b"../../target" ++ if request.param ++ else (tmp_path / "target").as_posix().encode("utf-8") ++ ) ++ traversal_file = "symlink/traversal_link/traversal.txt" ++ ++ record_lines = [f"{k},," for k in files] ++ record_lines.append(f"{symlink_entry},,") ++ record_lines.append(f"{traversal_file},,") ++ record_lines.append("symlink-0.1.dist-info/RECORD,,") ++ files["symlink-0.1.dist-info/RECORD"] = ("\n".join(record_lines) + "\n").encode() ++ ++ with zipfile.ZipFile(wheel, "w") as z: ++ for k, v in files.items(): ++ z.writestr(k, v) ++ ++ # Add a ZIP entry whose external attributes mark it as a symlink. ++ # The entry's data is the symlink target, pointing outside the ++ # installation directory. ++ info = zipfile.ZipInfo(symlink_entry) ++ info.create_system = 3 # unix ++ info.external_attr = (stat.S_IFLNK | 0o777) << 16 ++ z.writestr(info, symlink_target) ++ ++ z.writestr(traversal_file, b"path traversal") ++ ++ return wheel ++ ++ ++@pytest.mark.parametrize("existing", [False, True]) ++def test_no_path_traversal_via_symlink( ++ tmp_path: Path, env: MockEnv, wheel_with_symlink: Path, existing: bool ++) -> None: ++ target_dir = tmp_path / "target" ++ target_dir.mkdir() ++ target = target_dir / "traversal.txt" ++ if existing: ++ target.write_text("original", encoding="utf-8") ++ ++ installer = WheelInstaller(env) ++ with pytest.raises(FileNotFoundError if WINDOWS else NotADirectoryError): ++ installer.install(wheel_with_symlink) ++ ++ traversal_link = Path(env.paths["purelib"]) / "symlink" / "traversal_link" ++ assert traversal_link.exists() ++ assert not traversal_link.is_symlink() # not even extracted as symlink ++ assert target_dir.exists() ++ if existing: ++ assert target.read_text(encoding="utf-8") == "original" ++ else: ++ assert not list(target_dir.iterdir()) +-- +2.47.3 + diff -Nru poetry-2.1.2+dfsg/debian/patches/series poetry-2.1.2+dfsg/debian/patches/series --- poetry-2.1.2+dfsg/debian/patches/series 2025-04-03 23:49:29.000000000 +0000 +++ poetry-2.1.2+dfsg/debian/patches/series 2026-07-03 15:25:04.000000000 +0000 @@ -2,3 +2,5 @@ fix-env-manager.patch fix-tests.patch increase-sleep-time-in-threading-test.patch +0001-installer-fix-path-traversal-10792.patch +0002-perf-use-os.path.abspath-instead-of-Path.resolve-108.patch