Package: release.debian.org
Severity: normal
Tags: trixie
X-Debbugs-Cc: [email protected], [email protected]
Control: affects -1 + src:poetry
User: [email protected]
Usertags: pu
* CVE-2026-34591: Wheel Path Traversal Leading to Arbitrary File Write
(Closes: #1132609)
diffstat for poetry-2.1.2+dfsg poetry-2.1.2+dfsg
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(+)
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-04 02:49:58.000000000 +0300
+++ poetry-2.1.2+dfsg/debian/changelog 2026-07-03 18:25:04.000000000 +0300
@@ -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 <[email protected]> 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 02:00:00.000000000 +0200
+++
poetry-2.1.2+dfsg/debian/patches/0001-installer-fix-path-traversal-10792.patch
2026-07-03 18:24:45.000000000 +0300
@@ -0,0 +1,90 @@
+From 6895943e9e2f9ce563ea51b984830fb87f4d7371 Mon Sep 17 00:00:00 2001
+From: Randy Döring <[email protected]>
+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()
++
++
[email protected]
++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 02:00:00.000000000 +0200
+++
poetry-2.1.2+dfsg/debian/patches/0002-perf-use-os.path.abspath-instead-of-Path.resolve-108.patch
2026-07-03 18:24:45.000000000 +0300
@@ -0,0 +1,185 @@
+From bd6f54aec6e4f07dcf77ad9e8acee474e55cb06a Mon Sep 17 00:00:00 2001
+From: Randy Döring <[email protected]>
+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()
+
+
[email protected]
+-def wheel_with_path_traversal(tmp_path: Path) -> Path:
[email protected](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:
[email protected]("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()
++
++
[email protected](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
++
++
[email protected]("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-04 02:49:29.000000000
+0300
+++ poetry-2.1.2+dfsg/debian/patches/series 2026-07-03 18:25:04.000000000
+0300
@@ -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