Control: tags 1132609 + patch
Control: tags 1132609 + pending

Dear maintainer,

I've prepared an NMU for poetry (versioned as 2.3.4-0.1) and uploaded
it to DELAYED/2. Please feel free to tell me if I should cancel it.

cu
Adrian
diffstat for poetry-2.3.2+dfsg poetry-2.3.4

 CHANGELOG.md                               |   47 +++++++-
 debian/changelog                           |    9 +
 docs/_index.md                             |   16 ++
 docs/cli.md                                |   36 ++++--
 docs/pyproject.md                          |   51 ++++++++
 poetry.lock                                |   10 -
 pyproject.toml                             |    4 
 src/poetry/console/commands/env/remove.py  |    5 
 src/poetry/console/commands/publish.py     |   20 ++-
 src/poetry/console/commands/python/list.py |    6 -
 src/poetry/installation/wheel_installer.py |   22 +++
 src/poetry/layouts/layout.py               |    4 
 src/poetry/utils/_compat.py                |   28 ++++
 src/poetry/utils/authenticator.py          |    8 -
 src/poetry/utils/env/env_manager.py        |   26 ++--
 src/poetry/utils/env/python/manager.py     |    5 
 src/poetry/utils/helpers.py                |   37 ++++++
 src/poetry/vcs/git/backend.py              |   20 ++-
 tests/conftest.py                          |   82 ++++++++++++++
 tests/console/commands/conftest.py         |    4 
 tests/console/commands/test_init.py        |   30 ++---
 tests/console/commands/test_publish.py     |   29 +++++
 tests/installation/test_wheel_installer.py |   65 +++++++++++
 tests/utils/env/conftest.py                |   11 +
 tests/utils/env/test_env_manager.py        |   78 +++++++++++++
 tests/utils/test_compat.py                 |   71 ++++++++++++
 tests/utils/test_helpers.py                |  167 +++++++++++++++++++++++++++++
 tests/vcs/git/test_backend.py              |  134 +++++++++++++++++++++++
 28 files changed, 945 insertions(+), 80 deletions(-)

diff -Nru poetry-2.3.2+dfsg/CHANGELOG.md poetry-2.3.4/CHANGELOG.md
--- poetry-2.3.2+dfsg/CHANGELOG.md	2026-02-01 17:31:39.000000000 +0200
+++ poetry-2.3.4/CHANGELOG.md	2026-04-12 18:09:54.000000000 +0300
@@ -1,5 +1,48 @@
 # Change Log
 
+## [2.3.4] - 2026-04-12
+
+### Fixed
+
+- Fix a performance regression in the wheel installer that was introduced in Poetry 2.3.3 ([#10821](https://github.com/python-poetry/poetry/pull/10821)).
+- Fix a path traversal vulnerability in sdist extraction on Python 3.10.0-3.10.12 and 3.11.0-3.11.4 that could allow malicious tarball files to write files outside the target directory ([#10837](https://github.com/python-poetry/poetry/pull/10837)).
+
+
+## [2.3.3] - 2026-03-29
+
+### Fixed
+
+- **Fix a path traversal vulnerability in the wheel installer that could allow malicious wheel files to write files outside the intended installation directory** ([#10792](https://github.com/python-poetry/poetry/pull/10792)).
+- Fix an issue where `git` dependencies from annotated tags could not be updated ([#10719](https://github.com/python-poetry/poetry/pull/10719)).
+- Fix an issue where empty `VIRTUAL_ENV` or `CONDA_PREFIX` environment variables (e.g., after `conda deactivate`) would cause Poetry to incorrectly detect an active virtualenv ([#10784](https://github.com/python-poetry/poetry/pull/10784)).
+- Fix an issue where an incomprehensible error message was printed when `.venv` was a file instead of a directory ([#10777](https://github.com/python-poetry/poetry/pull/10777)).
+- Fix an issue where HTTP Basic Authentication credentials could be corrupted during request preparation, causing authentication failures with long tokens ([#10748](https://github.com/python-poetry/poetry/pull/10748)).
+- Fix an issue where `poetry publish --no-interaction --build` requested user interaction ([#10769](https://github.com/python-poetry/poetry/pull/10769)).
+- Fix an issue where `poetry init` and `poetry new` created a deprecated `project.license` format ([#10787](https://github.com/python-poetry/poetry/pull/10787)).
+
+### Docs
+
+- Clarify the differences between `poetry install` and `poetry update` ([#10713](https://github.com/python-poetry/poetry/pull/10713)).
+- Clarify the section of fields in the `pyproject.toml` examples ([#10753](https://github.com/python-poetry/poetry/pull/10753)).
+- Add a note about the different installation location when Python from the Microsoft Store is used ([#10759](https://github.com/python-poetry/poetry/pull/10759)).
+- Fix the system requirements for Poetry ([#10739](https://github.com/python-poetry/poetry/pull/10739)).
+- Fix the `poetry cache clear` example ([#10749](https://github.com/python-poetry/poetry/pull/10749)).
+- Fix the link to `pipx` installation instructions ([#10783](https://github.com/python-poetry/poetry/pull/10783)).
+
+### poetry-core ([`2.3.2`](https://github.com/python-poetry/poetry-core/releases/tag/2.3.2))
+
+- Fix an issue where `platform_release` could not be parsed on Debian Trixie ([#930](https://github.com/python-poetry/poetry-core/pull/930)).
+- Fix an issue where using `project.readme.text` in the `pyproject.toml` file resulted in broken metadata ([#914](https://github.com/python-poetry/poetry-core/pull/914)).
+- Fix an issue where dependency groups were considered equal when their resolved dependencies were equal, even if the groups themselves were not ([#919](https://github.com/python-poetry/poetry-core/pull/919)).
+- Fix an issue where removing a dependency from a group that included another group resulted in other dependencies being added to the included group ([#922](https://github.com/python-poetry/poetry-core/pull/922)).
+- Fix an issue where PEP 735 `include-group` entries were lost when `[tool.poetry.group]` also defined `include-groups` for the same group ([#924](https://github.com/python-poetry/poetry-core/pull/924)).
+- Fix an issue where the union of `<value> not in <marker>` constraints was wrongly treated as always satisfied ([#925](https://github.com/python-poetry/poetry-core/pull/925)).
+- Fix an issue where a post release with a local version identifier was wrongly allowed by a `>` version constraint ([#921](https://github.com/python-poetry/poetry-core/pull/921)).
+- Fix an issue where a version with the local version identifier `0` was treated as equal to the corresponding public version ([#920](https://github.com/python-poetry/poetry-core/pull/920)).
+- Fix an issue where a `!= <version>` constraint wrongly disallowed pre releases and post releases of the specified version ([#929](https://github.com/python-poetry/poetry-core/pull/929)).
+- Fix an issue where `in` and `not in` constraints were wrongly not allowed by specific compound constraints ([#927](https://github.com/python-poetry/poetry-core/pull/927)).
+
+
 ## [2.3.2] - 2026-02-01
 
 ### Changed
@@ -2658,7 +2701,9 @@
 
 
 
-[Unreleased]: https://github.com/python-poetry/poetry/compare/2.3.2...main
+[Unreleased]: https://github.com/python-poetry/poetry/compare/2.3.4...main
+[2.3.4]: https://github.com/python-poetry/poetry/releases/tag/2.3.4
+[2.3.3]: https://github.com/python-poetry/poetry/releases/tag/2.3.3
 [2.3.2]: https://github.com/python-poetry/poetry/releases/tag/2.3.2
 [2.3.1]: https://github.com/python-poetry/poetry/releases/tag/2.3.1
 [2.3.0]: https://github.com/python-poetry/poetry/releases/tag/2.3.0
diff -Nru poetry-2.3.2+dfsg/debian/changelog poetry-2.3.4/debian/changelog
--- poetry-2.3.2+dfsg/debian/changelog	2026-02-22 22:15:15.000000000 +0200
+++ poetry-2.3.4/debian/changelog	2026-06-21 00:00:28.000000000 +0300
@@ -1,3 +1,12 @@
+poetry (2.3.4-0.1) unstable; urgency=medium
+
+  * Non-maintainer upload.
+  * New upstream release.
+    - CVE-2026-34591: Wheel Path Traversal Leading to Arbitrary File Write
+      (Closes: #1132609)
+
+ -- Adrian Bunk <[email protected]>  Sun, 21 Jun 2026 00:00:28 +0300
+
 poetry (2.3.2+dfsg-3) unstable; urgency=medium
 
   * Team upload.
diff -Nru poetry-2.3.2+dfsg/docs/cli.md poetry-2.3.4/docs/cli.md
--- poetry-2.3.2+dfsg/docs/cli.md	2026-02-01 17:31:39.000000000 +0200
+++ poetry-2.3.4/docs/cli.md	2026-04-12 18:09:54.000000000 +0300
@@ -263,7 +263,7 @@
 To only remove a specific package from a cache, you have to specify the cache entry in the following form `cache:package:version`:
 
 ```bash
-poetry cache clear pypi:requests:2.24.0
+poetry cache clear PyPI:requests:2.24.0
 ```
 
 ### cache list
@@ -475,14 +475,6 @@
 The `install` command reads the `pyproject.toml` file from the current project,
 resolves the dependencies, and installs them.
 
-{{% note %}}
-Normally, you should prefer `poetry sync` to `poetry install` to avoid untracked outdated packages.
-However, if you have set `virtualenvs.create = false` to install dependencies into your system environment,
-which is discouraged, or `virtualenvs.options.system-site-packages = true` to make
-system site-packages available in your virtual environment, you should use `poetry install`
-because `poetry sync` will normally not work well in these cases.
-{{% /note %}}
-
 ```bash
 poetry install
 ```
@@ -493,6 +485,23 @@
 
 If there is no `poetry.lock` file, Poetry will create one after dependency resolution.
 
+{{% note %}}
+**When to use `install` vs `update`:**
+- Use `poetry install` to install dependencies as specified in `poetry.lock` (or resolve dependencies and create the lock file if it is missing).
+  This is what you run after cloning a project. For reproducible installs, prefer `poetry sync`,
+  which also removes packages that are not in the lock file.
+- Use `poetry update` when you want to update dependencies to their latest versions (within the constraints from the `pyproject.toml`)
+  and refresh `poetry.lock`.
+{{% /note %}}
+
+{{% note %}}
+Normally, you should prefer `poetry sync` to `poetry install` to avoid untracked outdated packages.
+However, if you have set `virtualenvs.create = false` to install dependencies into your system environment,
+which is discouraged, or `virtualenvs.options.system-site-packages = true` to make
+system site-packages available in your virtual environment, you should use `poetry install`
+because `poetry sync` will normally not work well in these cases.
+{{% /note %}}
+
 If you want to exclude one or more dependency groups for the installation, you can use
 the `--without` option.
 
@@ -1276,7 +1285,14 @@
 poetry update
 ```
 
-This will resolve all dependencies of the project and write the exact versions into `poetry.lock`.
+This will resolve all dependencies of the project, write the exact versions into `poetry.lock`,
+and install them into your environment.
+
+{{% note %}}
+The `update` command **does not** modify your `pyproject.toml` file. It only updates the
+`poetry.lock` file with the latest compatible versions based on the constraints already
+defined in `pyproject.toml`. To change version constraints, use the `add` command instead.
+{{% /note %}}
 
 If you just want to update a few packages and not all, you can list them as such:
 
diff -Nru poetry-2.3.2+dfsg/docs/_index.md poetry-2.3.4/docs/_index.md
--- poetry-2.3.2+dfsg/docs/_index.md	2026-02-01 17:31:39.000000000 +0200
+++ poetry-2.3.4/docs/_index.md	2026-04-12 18:09:54.000000000 +0300
@@ -18,7 +18,7 @@
 
 ## System requirements
 
-Poetry requires **Python 3.9+**. It is multi-platform and the goal is to make it work equally well
+Poetry requires **Python 3.10+**. It is multi-platform and the goal is to make it work equally well
 on Linux, macOS and Windows.
 
 ## Installation
@@ -40,7 +40,7 @@
 **Install pipx**
 
 If `pipx` is not already installed, you can follow any of the options in the
-[official pipx installation instructions](https://pipx.pypa.io/stable/installation/).
+[official pipx installation instructions](https://pipx.pypa.io/stable/how-to/install-pipx/).
 Any non-ancient version of `pipx` will do.
 
 {{< /step >}}
@@ -190,6 +190,18 @@
 - `%APPDATA%\Python\Scripts` on Windows.
 - `$POETRY_HOME/bin` if `$POETRY_HOME` is set.
 
+{{% note %}}
+If you have installed Python through the Microsoft Store, the `poetry` binary
+will be installed to a different location, for example:
+
+```
+%LOCALAPPDATA%\Packages\PythonSoftwareFoundation.Python.3.12_qbz5n2kfra8p0
+\LocalCache\Roaming\Python\Scripts
+```
+
+Replace `3.12` with your installed Python version and `qbz5n2kfra8p0` with your suffix.
+{{% /note %}}
+
 If this directory is not present in your `$PATH`, you can add it in order to invoke Poetry
 as `poetry`.
 
diff -Nru poetry-2.3.2+dfsg/docs/pyproject.md poetry-2.3.4/docs/pyproject.md
--- poetry-2.3.2+dfsg/docs/pyproject.md	2026-02-01 17:31:39.000000000 +0200
+++ poetry-2.3.4/docs/pyproject.md	2026-04-12 18:09:54.000000000 +0300
@@ -35,6 +35,7 @@
 
 
 ```toml
+[project]
 name = "my-package"
 ```
 
@@ -45,6 +46,8 @@
 This should be a valid [PEP 440](https://peps.python.org/pep-0440/) string.
 
 ```toml
+[project]
+# ...
 version = "0.1.0"
 ```
 
@@ -66,6 +69,8 @@
 A short description of the package.
 
 ```toml
+[project]
+# ...
 description = "A short description of the package."
 ```
 
@@ -94,6 +99,8 @@
 More identifiers are listed at the [SPDX Open Source License Registry](https://spdx.org/licenses/).
 
 ```toml
+[project]
+# ...
 license = "MIT"
 ```
 
@@ -136,6 +143,8 @@
 A path to the README file or the content.
 
 ```toml
+[project]
+# ...
 readme = "README.md"
 ```
 
@@ -159,6 +168,8 @@
 The Python version requirements of the project.
 
 ```toml
+[project]
+# ...
 requires-python = ">=3.8"
 ```
 
@@ -184,6 +195,8 @@
 This is a list of authors and should contain at least one author.
 
 ```toml
+[project]
+# ...
 authors = [
     { name = "Sébastien Eustace", email = "[email protected]" },
 ]
@@ -196,6 +209,8 @@
 This is a list of maintainers and should be distinct from authors.
 
 ```toml
+[project]
+# ...
 maintainers = [
     { name = "John Smith", email = "[email protected]" },
     { name = "Jane Smith", email = "[email protected]" },
@@ -207,6 +222,8 @@
 A list of keywords that the package is related to.
 
 ```toml
+[project]
+# ...
 keywords = [ "packaging", "poetry" ]
 ```
 
@@ -215,6 +232,8 @@
 A list of PyPI [trove classifiers](https://pypi.org/classifiers/) that describe the project.
 
 ```toml
+[project]
+# ...
 classifiers = [
     "Topic :: Software Development :: Build Tools",
     "Topic :: Software Development :: Libraries :: Python Modules"
@@ -326,6 +345,8 @@
 The `dependencies` of the project.
 
 ```toml
+[project]
+# ...
 dependencies = [
     "requests>=2.13.0",
 ]
@@ -366,6 +387,8 @@
 See [basic usage]({{< relref "basic-usage#operating-modes" >}}) for more information.
 
 ```toml
+[tool.poetry]
+# ...
 package-mode = false
 ```
 
@@ -379,6 +402,7 @@
 
 
 ```toml
+[tool.poetry]
 name = "my-package"
 ```
 
@@ -395,6 +419,8 @@
 This should be a valid [PEP 440](https://peps.python.org/pep-0440/) string.
 
 ```toml
+[tool.poetry]
+# ...
 version = "0.1.0"
 ```
 
@@ -412,6 +438,8 @@
 A short description of the package.
 
 ```toml
+[tool.poetry]
+# ...
 description = "A short description of the package."
 ```
 
@@ -441,6 +469,8 @@
 More identifiers are listed at the [SPDX Open Source License Registry](https://spdx.org/licenses/).
 
 ```toml
+[tool.poetry]
+# ...
 license = "MIT"
 ```
 
@@ -453,6 +483,8 @@
 This is a list of authors and should contain at least one author. Authors must be in the form `name <email>`.
 
 ```toml
+[tool.poetry]
+# ...
 authors = [
     "Sébastien Eustace <[email protected]>",
 ]
@@ -467,6 +499,8 @@
 This is a list of maintainers and should be distinct from authors. Maintainers may contain an email and be in the form `name <email>`.
 
 ```toml
+[tool.poetry]
+# ...
 maintainers = [
     "John Smith <[email protected]>",
     "Jane Smith <[email protected]>",
@@ -513,9 +547,11 @@
 
 **Deprecated**: Use `project.urls` instead.
 
-An URL to the website of the project.
+A URL to the website of the project.
 
 ```toml
+[tool.poetry]
+# ...
 homepage = "https://python-poetry.org/";
 ```
 
@@ -523,9 +559,11 @@
 
 **Deprecated**: Use `project.urls` instead.
 
-An URL to the repository of the project.
+A URL to the repository of the project.
 
 ```toml
+[tool.poetry]
+# ...
 repository = "https://github.com/python-poetry/poetry";
 ```
 
@@ -533,9 +571,11 @@
 
 **Deprecated**: Use `project.urls` instead.
 
-An URL to the documentation of the project.
+A URL to the documentation of the project.
 
 ```toml
+[tool.poetry]
+# ...
 documentation = "https://python-poetry.org/docs/";
 ```
 
@@ -546,6 +586,8 @@
 A list of keywords that the package is related to.
 
 ```toml
+[tool.poetry]
+# ...
 keywords = ["packaging", "poetry"]
 ```
 
@@ -642,6 +684,8 @@
 another package named `extra_package`, you will need to specify `my_package` explicitly:
 
 ```toml
+[tool.poetry]
+# ...
 packages = [
     { include = "my_package" },
     { include = "extra_package" },
@@ -921,6 +965,7 @@
 
 ```toml
 [tool.poetry]
+# ...
 requires-poetry = ">=2.0"
 ```
 
diff -Nru poetry-2.3.2+dfsg/poetry.lock poetry-2.3.4/poetry.lock
--- poetry-2.3.2+dfsg/poetry.lock	2026-02-01 17:31:39.000000000 +0200
+++ poetry-2.3.4/poetry.lock	2026-04-12 18:09:54.000000000 +0300
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 2.3.1 and should not be changed by hand.
+# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand.
 
 [[package]]
 name = "anyio"
@@ -1347,14 +1347,14 @@
 
 [[package]]
 name = "poetry-core"
-version = "2.3.1"
+version = "2.3.2"
 description = "Poetry PEP 517 Build Backend"
 optional = false
 python-versions = "<4.0,>=3.10"
 groups = ["main"]
 files = [
-    {file = "poetry_core-2.3.1-py3-none-any.whl", hash = "sha256:db1cf63b782570deb38bfba61e2304a553eef0740dc17959a50cc0f5115ee634"},
-    {file = "poetry_core-2.3.1.tar.gz", hash = "sha256:96f791d5d7d4e040f3983d76779425cf9532690e2756a24fd5ca0f86af19ef82"},
+    {file = "poetry_core-2.3.2-py3-none-any.whl", hash = "sha256:23df641b64f87fbb4ce1873c1915a4d4bb1b7d808c596e4307edc073e68d7234"},
+    {file = "poetry_core-2.3.2.tar.gz", hash = "sha256:20cb71be27b774628da9f384effd9183dfceb53bcef84063248a8672aa47031f"},
 ]
 
 [[package]]
@@ -2196,4 +2196,4 @@
 [metadata]
 lock-version = "2.1"
 python-versions = ">=3.10,<4.0"
-content-hash = "9ce6acbe11341f1c5fc0a8df9bfe5fe9237fedb54c90a756baf344b1c2b62e43"
+content-hash = "69cddd52ffdedc12c4b7445a9f359d67a4a1dc4b5d139d261a2c228a832bf39a"
diff -Nru poetry-2.3.2+dfsg/pyproject.toml poetry-2.3.4/pyproject.toml
--- poetry-2.3.2+dfsg/pyproject.toml	2026-02-01 17:31:39.000000000 +0200
+++ poetry-2.3.4/pyproject.toml	2026-04-12 18:09:54.000000000 +0300
@@ -1,10 +1,10 @@
 [project]
 name = "poetry"
-version = "2.3.2"
+version = "2.3.4"
 description = "Python dependency management and packaging made easy."
 requires-python = ">=3.10,<4.0"
 dependencies = [
-    "poetry-core (==2.3.1)",
+    "poetry-core (==2.3.2)",
     "build (>=1.2.1,<2.0.0)",
     "cachecontrol[filecache] (>=0.14.0,<0.15.0)",
     "cleo (>=2.1.0,<3.0.0)",
diff -Nru poetry-2.3.2+dfsg/src/poetry/console/commands/env/remove.py poetry-2.3.4/src/poetry/console/commands/env/remove.py
--- poetry-2.3.2+dfsg/src/poetry/console/commands/env/remove.py	2026-02-01 17:31:39.000000000 +0200
+++ poetry-2.3.4/src/poetry/console/commands/env/remove.py	2026-04-12 18:09:54.000000000 +0300
@@ -7,6 +7,7 @@
 from cleo.helpers import option
 
 from poetry.console.commands.command import Command
+from poetry.utils._compat import is_relative_to
 
 
 if TYPE_CHECKING:
@@ -54,8 +55,8 @@
             self.line(f"Deleted virtualenv: <comment>{venv.path}</comment>")
         if remove_all_envs or is_in_project:
             for venv in manager.list():
-                if not is_in_project or venv.path.is_relative_to(
-                    self.poetry.pyproject_path.parent
+                if not is_in_project or is_relative_to(
+                    venv.path, self.poetry.pyproject_path.parent
                 ):
                     manager.remove_venv(venv.path)
                     self.line(f"Deleted virtualenv: <comment>{venv.path}</comment>")
diff -Nru poetry-2.3.2+dfsg/src/poetry/console/commands/publish.py poetry-2.3.4/src/poetry/console/commands/publish.py
--- poetry-2.3.2+dfsg/src/poetry/console/commands/publish.py	2026-02-01 17:31:39.000000000 +0200
+++ poetry-2.3.4/src/poetry/console/commands/publish.py	2026-04-12 18:09:54.000000000 +0300
@@ -72,13 +72,21 @@
 
         # Building package first, if told
         if self.option("build"):
-            if publisher.files and not self.confirm(
-                f"There are <info>{len(publisher.files)}</info> files ready for"
-                " publishing. Build anyway?"
-            ):
-                self.line_error("<error>Aborted!</error>")
+            if publisher.files:
+                if self.io.is_interactive():
+                    if not self.confirm(
+                        f"There are <info>{len(publisher.files)}</info> files ready for"
+                        f" publishing in {dist_dir}. Build anyway?"
+                    ):
+                        self.line_error("<error>Aborted!</error>")
 
-                return 1
+                        return 1
+
+                else:
+                    self.line(
+                        f"<warning>Warning: There are <info>{len(publisher.files)}</info> files "
+                        f"ready for publishing in {dist_dir}. Build anyway!</warning>"
+                    )
 
             self.call("build", args=f"--output {dist_dir}")
 
diff -Nru poetry-2.3.2+dfsg/src/poetry/console/commands/python/list.py poetry-2.3.4/src/poetry/console/commands/python/list.py
--- poetry-2.3.2+dfsg/src/poetry/console/commands/python/list.py	2026-02-01 17:31:39.000000000 +0200
+++ poetry-2.3.4/src/poetry/console/commands/python/list.py	2026-04-12 18:09:54.000000000 +0300
@@ -10,6 +10,7 @@
 
 from poetry.config.config import Config
 from poetry.console.commands.command import Command
+from poetry.utils._compat import is_relative_to
 from poetry.utils.env.python import Python
 
 
@@ -107,9 +108,8 @@
             implementation = implementations.get(
                 pv.implementation.lower(), pv.implementation
             )
-            is_poetry_managed = (
-                pv.executable is None
-                or pv.executable.resolve().is_relative_to(python_installation_path)
+            is_poetry_managed = pv.executable is None or is_relative_to(
+                pv.executable.resolve(), python_installation_path
             )
 
             if self.option("managed") and not is_poetry_managed:
diff -Nru poetry-2.3.2+dfsg/src/poetry/installation/wheel_installer.py poetry-2.3.4/src/poetry/installation/wheel_installer.py
--- poetry-2.3.2+dfsg/src/poetry/installation/wheel_installer.py	2026-02-01 17:31:39.000000000 +0200
+++ poetry-2.3.4/src/poetry/installation/wheel_installer.py	2026-04-12 18:09:54.000000000 +0300
@@ -1,6 +1,7 @@
 from __future__ import annotations
 
 import logging
+import os
 import platform
 import sys
 
@@ -44,7 +45,26 @@
         from installer.utils import copyfileobj_with_hashing
         from installer.utils import make_file_executable
 
-        target_path = Path(self.scheme_dict[scheme]) / path
+        # 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(
+                f"Attempting to write {path} outside of the target directory\n"
+                f"Target directory: {target_dir}\n"
+                f"Target path: {target_path}"
+            )
+
         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 -Nru poetry-2.3.2+dfsg/src/poetry/layouts/layout.py poetry-2.3.4/src/poetry/layouts/layout.py
--- poetry-2.3.2+dfsg/src/poetry/layouts/layout.py	2026-02-01 17:31:39.000000000 +0200
+++ poetry-2.3.4/src/poetry/layouts/layout.py	2026-04-12 18:09:54.000000000 +0300
@@ -32,7 +32,7 @@
 description = ""
 authors = [
 ]
-license = {}
+license = ""
 readme = ""
 requires-python = ""
 dependencies = [
@@ -158,7 +158,7 @@
             project_content["authors"].append(author)
 
         if self._license:
-            project_content["license"]["text"] = self._license
+            project_content["license"] = self._license
         else:
             project_content.remove("license")
 
diff -Nru poetry-2.3.2+dfsg/src/poetry/utils/authenticator.py poetry-2.3.4/src/poetry/utils/authenticator.py
--- poetry-2.3.2+dfsg/src/poetry/utils/authenticator.py	2026-02-01 17:31:39.000000000 +0200
+++ poetry-2.3.4/src/poetry/utils/authenticator.py	2026-04-12 18:09:54.000000000 +0300
@@ -189,13 +189,15 @@
         self, method: str, url: str, raise_for_status: bool = True, **kwargs: Any
     ) -> requests.Response:
         headers = kwargs.get("headers")
-        request = requests.Request(method, url, headers=headers)
         credential = self.get_credentials_for_url(url)
 
+        auth = None
         if credential.username is not None or credential.password is not None:
-            request = requests.auth.HTTPBasicAuth(
+            auth = requests.auth.HTTPBasicAuth(
                 credential.username or "", credential.password or ""
-            )(request)
+            )
+
+        request = requests.Request(method, url, headers=headers, auth=auth)
 
         session = self.get_session(url=url)
         prepared_request = session.prepare_request(request)
diff -Nru poetry-2.3.2+dfsg/src/poetry/utils/_compat.py poetry-2.3.4/src/poetry/utils/_compat.py
--- poetry-2.3.2+dfsg/src/poetry/utils/_compat.py	2026-02-01 17:31:39.000000000 +0200
+++ poetry-2.3.4/src/poetry/utils/_compat.py	2026-04-12 18:09:54.000000000 +0300
@@ -5,6 +5,7 @@
 import warnings
 
 from contextlib import suppress
+from pathlib import Path
 
 
 if sys.version_info < (3, 11):
@@ -52,6 +53,33 @@
         return locale.getencoding()
 
 
+def is_relative_to(path1: Path, path2: Path) -> bool:
+    """
+    Checks if path1 is relative to path2.
+
+    Works also if one of both paths has a Windows long path prefix.
+    A long path prefix may be added when calling Path.resolve().
+    """
+    if WINDOWS:
+        # Work around an issue that is_relative_to() does not work if
+        # one of both paths has a long path prefix and the other path has not.
+        long_path_prefix = "\\\\?\\"
+        long_path_unc_prefix = f"{long_path_prefix}UNC\\"
+
+        def remove_long_path_prefix(path: Path) -> Path:
+            if (path_str := str(path)).startswith(long_path_prefix):
+                if path_str.startswith(long_path_unc_prefix):
+                    path = Path("\\\\" + path_str.removeprefix(long_path_unc_prefix))
+                else:
+                    path = Path(path_str.removeprefix(long_path_prefix))
+            return path
+
+        path1 = remove_long_path_prefix(path1)
+        path2 = remove_long_path_prefix(path2)
+
+    return path1.is_relative_to(path2)
+
+
 def __getattr__(name: str) -> object:
     if name == "metadata":
         warnings.warn(
diff -Nru poetry-2.3.2+dfsg/src/poetry/utils/env/env_manager.py poetry-2.3.4/src/poetry/utils/env/env_manager.py
--- poetry-2.3.2+dfsg/src/poetry/utils/env/env_manager.py	2026-02-01 17:31:39.000000000 +0200
+++ poetry-2.3.4/src/poetry/utils/env/env_manager.py	2026-04-12 18:09:54.000000000 +0300
@@ -125,7 +125,7 @@
         if self.use_in_project_venv():
             create = False
             venv = self.in_project_venv
-            if venv.exists():
+            if venv.is_dir():
                 # We need to check if the patch version is correct
                 _venv = VirtualEnv(venv)
                 current_patch = ".".join(str(v) for v in _venv.version_info[:3])
@@ -159,7 +159,7 @@
 
         # Create if needed
         if not venv.exists() or create:
-            in_venv = os.environ.get("VIRTUAL_ENV") is not None
+            in_venv = bool(os.environ.get("VIRTUAL_ENV"))
             if in_venv or not venv.exists():
                 create = True
 
@@ -214,7 +214,9 @@
         conda_env_name = os.environ.get("CONDA_DEFAULT_ENV")
         # It's probably not a good idea to pollute Conda's global "base" env, since
         # most users have it activated all the time.
-        in_venv = env_prefix is not None and conda_env_name != "base"
+        # Treat an empty env_prefix as if no virtualenv is active, since conda
+        # can leave CONDA_PREFIX set to an empty string after deactivation.
+        in_venv = bool(env_prefix) and conda_env_name != "base"
 
         if not in_venv or env is not None:
             # Checking if a local virtualenv exists
@@ -249,14 +251,8 @@
 
             return VirtualEnv(venv)
 
-        if env_prefix is not None:
-            prefix = Path(env_prefix)
-            base_prefix = None
-        else:
-            prefix = Path(sys.prefix)
-            base_prefix = self.get_base_prefix()
-
-        return VirtualEnv(prefix, base_prefix)
+        assert env_prefix
+        return VirtualEnv(Path(env_prefix))
 
     def list(self, name: str | None = None) -> list[VirtualEnv]:
         if name is None:
@@ -456,7 +452,7 @@
                     f"Invalid template string in 'virtualenvs.prompt' setting: {e}"
                 ) from e
 
-        if not venv.exists():
+        if not venv.is_dir():
             if create_venv is False:
                 self._io.write_error_line(
                     "<fg=black;bg=yellow>"
@@ -467,6 +463,12 @@
 
                 return self.get_system_env()
 
+            if venv.is_file():
+                self._io.write_error_line(
+                    f"<warning>{venv} is not a virtual environment but a file. Removing it.</warning>"
+                )
+                venv.unlink()
+
             self._io.write_error_line(
                 f"Creating virtualenv <c1>{name}</> in"
                 f" {venv_path if not WINDOWS else get_real_windows_path(venv_path)!s}"
diff -Nru poetry-2.3.2+dfsg/src/poetry/utils/env/python/manager.py poetry-2.3.4/src/poetry/utils/env/python/manager.py
--- poetry-2.3.2+dfsg/src/poetry/utils/env/python/manager.py	2026-02-01 17:31:39.000000000 +0200
+++ poetry-2.3.4/src/poetry/utils/env/python/manager.py	2026-04-12 18:09:54.000000000 +0300
@@ -23,6 +23,7 @@
 from poetry.core.constraints.version import VersionConstraint
 from poetry.core.constraints.version import parse_constraint
 
+from poetry.utils._compat import is_relative_to
 from poetry.utils.env.python.exceptions import NoCompatiblePythonVersionFoundError
 from poetry.utils.env.python.providers import PoetryPythonPathProvider
 from poetry.utils.env.python.providers import ShutilWhichPythonProvider
@@ -84,10 +85,10 @@
     @classmethod
     def find_all(cls) -> Iterator[Python]:
         venv_path: Path | None = (
-            Path(os.environ["VIRTUAL_ENV"]) if "VIRTUAL_ENV" in os.environ else None
+            Path(venv) if (venv := os.environ.get("VIRTUAL_ENV")) else None
         )
         for python in findpython.find_all():
-            if venv_path and python.executable.is_relative_to(venv_path):
+            if venv_path and is_relative_to(python.executable, venv_path):
                 continue
             yield cls(python=python)
 
diff -Nru poetry-2.3.2+dfsg/src/poetry/utils/helpers.py poetry-2.3.4/src/poetry/utils/helpers.py
--- poetry-2.3.2+dfsg/src/poetry/utils/helpers.py	2026-02-01 17:31:39.000000000 +0200
+++ poetry-2.3.4/src/poetry/utils/helpers.py	2026-04-12 18:09:54.000000000 +0300
@@ -415,7 +415,7 @@
     else:
         # These versions of python shipped with a broken tarfile data_filter, per
         # https://github.com/python/cpython/issues/107845.
-        broken_tarfile_filter = {(3, 9, 17), (3, 10, 12), (3, 11, 4)}
+        broken_tarfile_filter = {(3, 10, 12), (3, 11, 4)}
         with tarfile.open(source) as archive:
             if (
                 hasattr(tarfile, "data_filter")
@@ -423,4 +423,37 @@
             ):
                 archive.extractall(dest, filter="data")
             else:
-                archive.extractall(dest)
+                # Validate all member paths before extraction
+                #
+                # 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()!
+                dest = Path(os.path.abspath(dest))
+                safe_members = []
+                for member in archive.getmembers():
+                    member_path = Path(os.path.abspath(dest / member.name))
+                    if not member_path.is_relative_to(dest):
+                        raise ValueError(
+                            f"Refusing to extract {member.name}: "
+                            f"would write outside {dest}"
+                        )
+                    if member.issym():
+                        link_target = Path(
+                            os.path.abspath(member_path.parent / member.linkname)
+                        )
+                        if not link_target.is_relative_to(dest):
+                            raise ValueError(
+                                f"Refusing symlink {member.name}: "
+                                f"target {member.linkname} outside {dest}"
+                            )
+                    elif member.islnk():
+                        link_target = Path(os.path.abspath(dest / member.linkname))
+                        if not link_target.is_relative_to(dest):
+                            raise ValueError(
+                                f"Refusing hardlink {member.name}: "
+                                f"target {member.linkname} outside {dest}"
+                            )
+                    safe_members.append(member)
+                archive.extractall(dest, members=safe_members)
diff -Nru poetry-2.3.2+dfsg/src/poetry/vcs/git/backend.py poetry-2.3.4/src/poetry/vcs/git/backend.py
--- poetry-2.3.2+dfsg/src/poetry/vcs/git/backend.py	2026-02-01 17:31:39.000000000 +0200
+++ poetry-2.3.4/src/poetry/vcs/git/backend.py	2026-04-12 18:09:54.000000000 +0300
@@ -1,5 +1,6 @@
 from __future__ import annotations
 
+import contextlib
 import dataclasses
 import logging
 import os
@@ -20,6 +21,7 @@
 from dulwich.errors import NotGitRepository
 from dulwich.file import FileLocked
 from dulwich.index import IndexEntry
+from dulwich.object_store import peel_sha
 from dulwich.objects import ObjectID
 from dulwich.protocol import PEELED_TAG_SUFFIX
 from dulwich.refs import Ref
@@ -63,8 +65,9 @@
     "- the remote ({remote}) you have specified\n"
     "    - was misspelled\n"
     "    - does not exist\n"
-    "    - requires credentials that were either not configured or is incorrect\n"
-    "    - is in accessible due to network issues"
+    "    - requires credentials that were either not configured or are incorrect\n"
+    "    - contains Git submodules that require credentials that were either not configured or are incorrect\n"
+    "    - is inaccessible due to network issues"
 )
 ERROR_MESSAGE_FILE_LOCK = (
     "- another process is holding the file lock\n"
@@ -96,7 +99,7 @@
         Resolve the ref using the provided remote refs.
         """
         self._normalise(remote_refs=remote_refs, repo=repo)
-        self._set_head(remote_refs=remote_refs)
+        self._set_head(remote_refs=remote_refs, repo=repo)
 
     def _normalise(self, remote_refs: FetchPackResult, repo: Repo) -> None:
         """
@@ -142,7 +145,7 @@
                 self.revision = sha.decode("utf-8")
                 return
 
-    def _set_head(self, remote_refs: FetchPackResult) -> None:
+    def _set_head(self, remote_refs: FetchPackResult, repo: Repo) -> None:
         """
         Internal helper method to populate ref and set it's sha as the remote's head
         and default ref.
@@ -165,6 +168,15 @@
                 )
             head = remote_refs.refs[self.ref]
 
+            # Peel tag objects to get the underlying commit SHA.
+            # Annotated tags are Tag objects, not Commit objects. Operations like
+            # reset_index() expect HEAD to point to a Commit, so we must peel tags
+            # to extract the commit SHA they reference.
+            # Object not in store yet will be handled during fetch
+            if head is not None:
+                with contextlib.suppress(KeyError):
+                    head = peel_sha(repo.object_store, head)[1].id
+
         remote_refs.refs[self.ref] = remote_refs.refs[Ref(b"HEAD")] = head
 
     @property
diff -Nru poetry-2.3.2+dfsg/tests/conftest.py poetry-2.3.4/tests/conftest.py
--- poetry-2.3.2+dfsg/tests/conftest.py	2026-02-01 17:31:39.000000000 +0200
+++ poetry-2.3.4/tests/conftest.py	2026-04-12 18:09:54.000000000 +0300
@@ -1047,3 +1047,85 @@
         return bin_dir
 
     return register
+
+
[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_path: b"path traversal",
+        "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
+
+
[email protected](params=[False, True])  # relative path
+def wheel_with_path_traversal_via_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
diff -Nru poetry-2.3.2+dfsg/tests/console/commands/conftest.py poetry-2.3.4/tests/console/commands/conftest.py
--- poetry-2.3.2+dfsg/tests/console/commands/conftest.py	2026-02-01 17:31:39.000000000 +0200
+++ poetry-2.3.4/tests/console/commands/conftest.py	2026-04-12 18:09:54.000000000 +0300
@@ -30,7 +30,7 @@
 authors = [
     {name = "Your Name",email = "[email protected]"}
 ]
-license = {text = "MIT"}
+license = "MIT"
 readme = "README.md"
 requires-python = ">=3.6"
 """
@@ -55,7 +55,7 @@
 authors = [
     {name = "Your Name",email = "[email protected]"}
 ]
-license = {text = "MIT"}
+license = "MIT"
 readme = "README.md"
 requires-python = ">=3.6"
 dependencies = [
diff -Nru poetry-2.3.2+dfsg/tests/console/commands/test_init.py poetry-2.3.4/tests/console/commands/test_init.py
--- poetry-2.3.2+dfsg/tests/console/commands/test_init.py	2026-02-01 17:31:39.000000000 +0200
+++ poetry-2.3.4/tests/console/commands/test_init.py	2026-04-12 18:09:54.000000000 +0300
@@ -145,7 +145,7 @@
 authors = [
     {name = "Your Name",email = "[email protected]"}
 ]
-license = {text = "MIT"}
+license = "MIT"
 requires-python = ">=3.6"
 dependencies = [
     "pendulum (>=2.0.0,<3.0.0)",
@@ -201,7 +201,7 @@
 authors = [
     {name = "Your Name",email = "[email protected]"}
 ]
-license = {text = "MIT"}
+license = "MIT"
 requires-python = ">=3.6"
 """
 
@@ -269,7 +269,7 @@
 authors = [
     {name = "Your Name",email = "[email protected]"}
 ]
-license = {text = "MIT"}
+license = "MIT"
 requires-python = ">=3.6"
 dependencies = [
     "demo @ git+https://github.com/demo/demo.git";
@@ -364,7 +364,7 @@
 authors = [
     {name = "Your Name",email = "[email protected]"}
 ]
-license = {text = "MIT"}
+license = "MIT"
 requires-python = ">=3.6"
 dependencies = [
     "demo @ git+https://github.com/demo/demo.git@develop";
@@ -412,7 +412,7 @@
 authors = [
     {name = "Your Name",email = "[email protected]"}
 ]
-license = {text = "MIT"}
+license = "MIT"
 requires-python = ">=3.6"
 dependencies = [
     "demo @ git+https://github.com/demo/pyproject-demo.git";
@@ -467,7 +467,7 @@
 authors = [
     {{name = "Your Name",email = "[email protected]"}}
 ]
-license = {{text = "MIT"}}
+license = "MIT"
 requires-python = ">=3.6"
 dependencies = [
     "demo @ {demo_uri}"
@@ -521,7 +521,7 @@
 authors = [
     {{name = "Your Name",email = "[email protected]"}}
 ]
-license = {{text = "MIT"}}
+license = "MIT"
 requires-python = ">=3.6"
 dependencies = [
     "demo @ {demo_uri}"
@@ -576,7 +576,7 @@
 authors = [
     {{name = "Your Name",email = "[email protected]"}}
 ]
-license = {{text = "MIT"}}
+license = "MIT"
 requires-python = ">=3.6"
 dependencies = [
     "demo @ {demo_uri}"
@@ -623,7 +623,7 @@
 authors = [
     {name = "Your Name",email = "[email protected]"}
 ]
-license = {text = "MIT"}
+license = "MIT"
 requires-python = ">=3.8"
 dependencies = [
     "foo (==1.19.2)",
@@ -660,7 +660,7 @@
 authors = [
     {name = "Your Name",email = "[email protected]"}
 ]
-license = {text = "MIT"}
+license = "MIT"
 requires-python = ">=3.6"
 """
 
@@ -691,7 +691,7 @@
 authors = [
     {name = "Your Name",email = "[email protected]"}
 ]
-license = {text = "MIT"}
+license = "MIT"
 requires-python = ">=3.6"
 dependencies = [
     "pendulum (>=2.0.0,<3.0.0)"
@@ -733,7 +733,7 @@
 authors = [
     {name = "Your Name",email = "[email protected]"}
 ]
-license = {text = "MIT"}
+license = "MIT"
 requires-python = ">=3.6"
 dependencies = [
     "pendulum (>=2.0.0,<3.0.0)",
@@ -768,7 +768,7 @@
 authors = [
     {name = "Your Name",email = "[email protected]"}
 ]
-license = {text = "MIT"}
+license = "MIT"
 requires-python = ">=3.6"
 dependencies = [
 ]
@@ -814,7 +814,7 @@
 authors = [
     {name = "Your Name",email = "[email protected]"}
 ]
-license = {text = "MIT"}
+license = "MIT"
 requires-python = ">=3.6"
 dependencies = [
 ]
@@ -861,7 +861,7 @@
 authors = [
     {name = "Foo Bar",email = "[email protected]"}
 ]
-license = {text = "MIT"}
+license = "MIT"
 requires-python = ">=3.8"
 dependencies = [
     "pendulum (>=2.0.0,<3.0.0)"
diff -Nru poetry-2.3.2+dfsg/tests/console/commands/test_publish.py poetry-2.3.4/tests/console/commands/test_publish.py
--- poetry-2.3.2+dfsg/tests/console/commands/test_publish.py	2026-02-01 17:31:39.000000000 +0200
+++ poetry-2.3.4/tests/console/commands/test_publish.py	2026-04-12 18:09:54.000000000 +0300
@@ -5,6 +5,7 @@
 from pathlib import Path
 from typing import TYPE_CHECKING
 from typing import NoReturn
+from unittest.mock import PropertyMock
 
 import pytest
 import requests
@@ -215,3 +216,31 @@
     assert "Publishing simple-project (1.2.3) to PyPI" in output
     assert "- Uploading simple_project-1.2.3.tar.gz" in error
     assert "- Uploading simple_project-1.2.3-py2.py3-none-any.whl" in error
+
+
+def test_publish_build_no_interaction_skips_confirmation(
+    app_tester: ApplicationTester, mocker: MockerFixture
+) -> None:
+    mocker.patch(
+        "poetry.publishing.publisher.Publisher.files",
+        new_callable=PropertyMock,
+        return_value=[Path("dist/simple_project-1.2.3-py2.py3-none-any.whl")],
+    )
+    confirm = mocker.patch("poetry.console.commands.publish.PublishCommand.confirm")
+    command_call = mocker.patch("poetry.console.commands.publish.PublishCommand.call")
+    publisher_publish = mocker.patch("poetry.publishing.Publisher.publish")
+
+    exit_code = app_tester.execute("publish --build --no-interaction --dry-run")
+
+    assert exit_code == 0
+    output = app_tester.io.fetch_output()
+    error = app_tester.io.fetch_error()
+
+    confirm.assert_not_called()
+    assert "Build anyway?" not in output
+    assert "Build anyway?" not in error
+    assert (
+        "Warning: There are 1 files ready for publishing in dist. Build anyway!"
+    ) in output
+    command_call.assert_called_once_with("build", args="--output dist")
+    assert publisher_publish.call_count == 1
diff -Nru poetry-2.3.2+dfsg/tests/installation/test_wheel_installer.py poetry-2.3.4/tests/installation/test_wheel_installer.py
--- poetry-2.3.2+dfsg/tests/installation/test_wheel_installer.py	2026-02-01 17:31:39.000000000 +0200
+++ poetry-2.3.4/tests/installation/test_wheel_installer.py	2026-04-12 18:09:54.000000000 +0300
@@ -10,6 +10,7 @@
 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 @@
 
 @pytest.fixture
 def env(tmp_path: Path) -> MockEnv:
-    return MockEnv(path=tmp_path)
+    return MockEnv(path=tmp_path / "env")
 
 
 @pytest.fixture(scope="module")
@@ -81,3 +82,65 @@
         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]("existing", [False, True])
+def test_no_path_traversal(
+    env: MockEnv, wheel_with_path_traversal: Path, existing: bool
+) -> None:
+    """see also test_extractall_wheel_no_path_traversal in test_helpers.py"""
+    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)
+
+    if existing:
+        assert target.exists()
+        assert target.read_text(encoding="utf-8") == "original"
+    else:
+        assert not target.exists()
+
+
[email protected]("existing", [False, True])
+def test_no_path_traversal_via_symlink(
+    tmp_path: Path,
+    env: MockEnv,
+    wheel_with_path_traversal_via_symlink: Path,
+    existing: bool,
+) -> None:
+    """see also test_extractall_wheel_no_path_traversal_via_symlink
+    in test_helpers.py"""
+    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_path_traversal_via_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())
diff -Nru poetry-2.3.2+dfsg/tests/utils/env/conftest.py poetry-2.3.4/tests/utils/env/conftest.py
--- poetry-2.3.2+dfsg/tests/utils/env/conftest.py	2026-02-01 17:31:39.000000000 +0200
+++ poetry-2.3.4/tests/utils/env/conftest.py	2026-04-12 18:09:54.000000000 +0300
@@ -4,6 +4,8 @@
 
 import pytest
 
+from cleo.io.buffered_io import BufferedIO
+
 from poetry.utils.env import EnvManager
 
 
@@ -19,5 +21,10 @@
 
 
 @pytest.fixture
-def manager(poetry: Poetry) -> EnvManager:
-    return EnvManager(poetry)
+def io() -> BufferedIO:
+    return BufferedIO()
+
+
[email protected]
+def manager(poetry: Poetry, io: BufferedIO) -> EnvManager:
+    return EnvManager(poetry, io)
diff -Nru poetry-2.3.2+dfsg/tests/utils/env/test_env_manager.py poetry-2.3.4/tests/utils/env/test_env_manager.py
--- poetry-2.3.2+dfsg/tests/utils/env/test_env_manager.py	2026-02-01 17:31:39.000000000 +0200
+++ poetry-2.3.4/tests/utils/env/test_env_manager.py	2026-04-12 18:09:54.000000000 +0300
@@ -32,6 +32,7 @@
     from collections.abc import Iterator
     from unittest.mock import MagicMock
 
+    from cleo.io.buffered_io import BufferedIO
     from pytest import LogCaptureFixture
     from pytest_mock import MockerFixture
 
@@ -470,6 +471,56 @@
     assert not envs_file.exists()
 
 
+def test_activate_with_in_project_setting_if_venv_is_file(
+    manager: EnvManager,
+    poetry: Poetry,
+    io: BufferedIO,
+    config: Config,
+    tmp_path: Path,
+    mocker: MockerFixture,
+    venv_flags_default: dict[str, bool],
+    mocked_python_register: MockedPythonRegister,
+) -> None:
+    if "VIRTUAL_ENV" in os.environ:
+        del os.environ["VIRTUAL_ENV"]
+
+    config.merge(
+        {
+            "virtualenvs": {
+                "path": str(tmp_path / "virtualenvs"),
+                "in-project": True,
+            }
+        }
+    )
+
+    mocked_python_register("3.7.1")
+    m = mocker.patch("poetry.utils.env.EnvManager.build_venv")
+
+    venv_path = poetry.file.path.parent / ".venv"
+    assert not venv_path.exists()
+    venv_path.touch()
+    assert venv_path.is_file()
+
+    manager.activate("python3.7")
+
+    m.assert_called_with(
+        poetry.file.path.parent / ".venv",
+        executable=Path("/usr/bin/python3.7"),
+        flags=venv_flags_default,
+        prompt="simple-project-py3.7",
+    )
+
+    envs_file = TOMLFile(tmp_path / "virtualenvs" / "envs.toml")
+    assert not envs_file.exists()
+
+    # The .venv file is removed, but no .venv is created because we mocked build_venv.
+    assert not venv_path.exists()
+    assert (
+        f"{venv_path} is not a virtual environment but a file. Removing it."
+        in io.fetch_error()
+    )
+
+
 def test_deactivate_non_activated_but_existing(
     tmp_path: Path,
     manager: EnvManager,
@@ -577,6 +628,33 @@
     assert env.base == Path(sys.base_prefix)
 
 
[email protected]("env_var", ["VIRTUAL_ENV", "CONDA_PREFIX"])
+def test_get_ignores_empty_env_prefix(
+    manager: EnvManager,
+    poetry: Poetry,
+    in_project_venv_dir: Path,
+    env_var: str,
+    mocker: MockerFixture,
+) -> None:
+    """An empty VIRTUAL_ENV or CONDA_PREFIX should be treated as unset.
+
+    After ``conda deactivate``, conda can leave CONDA_PREFIX set to an
+    empty string.  Poetry should not consider that as an active
+    virtualenv and should fall back to the in-project .venv instead.
+
+    See: https://github.com/python-poetry/poetry/issues/10770
+    """
+    os.environ.pop("VIRTUAL_ENV", None)
+    os.environ.pop("CONDA_PREFIX", None)
+    os.environ[env_var] = ""
+    mocker.patch(
+        "poetry.utils.env.virtual_env.VirtualEnv.__init__",
+        lambda self, *args, **kwargs: setattr(self, "_path", args[0]),
+    )
+    venv = manager.get()
+    assert venv.path == in_project_venv_dir
+
+
 def test_list(
     tmp_path: Path,
     manager: EnvManager,
diff -Nru poetry-2.3.2+dfsg/tests/utils/test_compat.py poetry-2.3.4/tests/utils/test_compat.py
--- poetry-2.3.2+dfsg/tests/utils/test_compat.py	1970-01-01 02:00:00.000000000 +0200
+++ poetry-2.3.4/tests/utils/test_compat.py	2026-04-12 18:09:54.000000000 +0300
@@ -0,0 +1,71 @@
+from __future__ import annotations
+
+import sys
+
+from pathlib import Path
+
+import pytest
+
+from poetry.utils._compat import is_relative_to
+
+
[email protected](
+    ("path1", "path2", "expected"),
+    [
+        ("a", "a", True),
+        ("a/b", "a/b", True),
+        ("a/b", "a", True),
+        ("a", "a/b", False),
+        ("a/b/c/d", "a/b", True),
+        ("a/b", "a/b/c/d", False),
+    ],
+)
+def test_is_relative_to(path1: str, path2: str, expected: bool) -> None:
+    assert is_relative_to(Path(path1), Path(path2)) is expected
+
+
[email protected](
+    ("path1", "path2", "expected"),
+    [
+        ("/", "/", True),
+        ("/a/b", "/a/b", True),
+        ("/a/b", "/a", True),
+        ("/a", "/a/b", False),
+        ("/a/b/c/d", "/a/b", True),
+        ("/a/b", "/a/b/c/d", False),
+    ],
+)
[email protected](sys.platform == "win32", reason="non-Windows paths")
+def test_is_relative_to_non_win32(path1: str, path2: str, expected: bool) -> None:
+    assert is_relative_to(Path(path1), Path(path2)) is expected
+
+
[email protected](
+    ("path1", "path2", "expected"),
+    [
+        ("C:\\", "C:\\", True),
+        (r"C:\a\b", r"C:\a\b", True),
+        (r"C:\a\b", r"C:\a", True),
+        (r"C:\a", r"C:\a\b", False),
+        (r"C:\a\b\c\d", r"C:\a\b", True),
+        (r"C:\a\b", r"C:\a\b\c\d", False),
+        (r"C:\a\b", r"D:\a", False),
+        (r"C:\a\b", "D:\\", False),
+        (r"\\server\a\b", r"\\server\a", True),
+        (r"\\server\a", r"\\server\a\b", False),
+        (r"\\server2\a\b", r"\\server\a", False),
+        # long path prefix
+        (r"\\?\C:\a\b", r"\\?\C:\a", True),
+        (r"\\?\C:\a\b", r"C:\a", True),
+        (r"C:\a\b", r"\\?\C:\a", True),
+        (r"\\?\C:\a", r"\\?\C:\a\b", False),
+        # long path UNC prefix
+        (r"\\?\UNC\server\a\b", r"\\?\UNC\server\a", True),
+        (r"\\?\UNC\server\a\b", r"\\server\a", True),
+        (r"\\server\a\b", r"\\?\UNC\server\a", True),
+        (r"\\?\UNC\server\a", r"\\?\UNC\server\a\b", False),
+    ],
+)
[email protected](sys.platform != "win32", reason="Windows paths")
+def test_is_relative_to_win32(path1: str, path2: str, expected: bool) -> None:
+    assert is_relative_to(Path(path1), Path(path2)) is expected
diff -Nru poetry-2.3.2+dfsg/tests/utils/test_helpers.py poetry-2.3.4/tests/utils/test_helpers.py
--- poetry-2.3.2+dfsg/tests/utils/test_helpers.py	2026-02-01 17:31:39.000000000 +0200
+++ poetry-2.3.4/tests/utils/test_helpers.py	2026-04-12 18:09:54.000000000 +0300
@@ -1,7 +1,10 @@
 from __future__ import annotations
 
 import base64
+import contextlib
 import re
+import sys
+import tarfile
 
 from pathlib import Path
 from typing import TYPE_CHECKING
@@ -13,10 +16,12 @@
 from poetry.core.utils.helpers import parse_requires
 from requests.exceptions import ChunkedEncodingError
 
+from poetry.utils._compat import WINDOWS
 from poetry.utils.helpers import Downloader
 from poetry.utils.helpers import HTTPRangeRequestSupportedError
 from poetry.utils.helpers import download_file
 from poetry.utils.helpers import ensure_path
+from poetry.utils.helpers import extractall
 from poetry.utils.helpers import get_file_hash
 from poetry.utils.helpers import get_highest_priority_hash_type
 
@@ -341,3 +346,165 @@
 
     path.mkdir()
     assert ensure_path(path=path, is_directory=True) is path
+
+
[email protected]("relative", [False, True])
[email protected]("existing", [False, True])
+def test_extractall_sdist_no_path_traversal(
+    tmp_path: Path, relative: bool, existing: bool
+) -> None:
+    import io
+    import tarfile
+
+    archive = tmp_path / "traversal.tar.gz"
+    dest = tmp_path / "dest"
+    dest.mkdir()
+
+    target = tmp_path / "traversal.txt"
+    if existing:
+        target.write_text("original", encoding="utf-8")
+
+    with tarfile.open(archive, "w:gz") as tar:
+        b = b"path traversal"
+        t = tarfile.TarInfo("../traversal.txt" if relative else target.as_posix())
+        t.size = len(b)
+        tar.addfile(t, io.BytesIO(b))
+
+    has_data_filter = hasattr(tarfile, "data_filter")
+    # The stdlib implementation just strips the leading "/" from absolute paths
+    # and extracts them relative to the target directory (except for Windows).
+    # We do not care and raise an error.
+    raises = (
+        relative
+        or WINDOWS
+        or not has_data_filter
+        or sys.version_info[:3] in {(3, 10, 12), (3, 11, 4)}
+    )
+    exceptions: tuple[type[Exception], ...]
+    if has_data_filter:
+        if relative:
+            exceptions = (tarfile.OutsideDestinationError, ValueError)
+        else:
+            exceptions = (tarfile.AbsolutePathError, ValueError)
+    else:
+        # tarfile.OutsideDestinationError does not exist
+        exceptions = (ValueError,)
+
+    with pytest.raises(exceptions) if raises else contextlib.nullcontext():
+        extractall(source=archive, dest=dest, zip=False)
+
+    if existing:
+        assert target.exists()
+        assert target.read_text(encoding="utf-8") == "original"
+    else:
+        assert not target.exists()
+    if not raises:
+        # check that expected location exists, otherwise we have to check
+        # that there is no traversal in an unexpected location
+        assert (dest / target.as_posix().lstrip("/")).exists()
+
+
[email protected]("link_type", [tarfile.SYMTYPE, tarfile.LNKTYPE])
[email protected]("relative", [False, True])
[email protected]("existing", [False, True])
+def test_extractall_sdist_no_symlink_path_traversal(
+    tmp_path: Path, link_type: bytes, relative: bool, existing: bool
+) -> None:
+    import io
+    import tarfile
+
+    archive = tmp_path / "traversal.tar.gz"
+    dest = tmp_path / "dest"
+    dest.mkdir()
+
+    target = tmp_path / "traversal.txt"
+    if existing:
+        target.write_text("original", encoding="utf-8")
+
+    with tarfile.open(archive, "w:gz") as tar:
+        # We use a link in a subdirectory to test the difference
+        # between symlinks and hardlinks:
+        # symlinks are relative to the directory of the symlink,
+        # while hardlinks are relative to the root of the archive
+        s = tarfile.TarInfo("sub/link")
+        s.type = link_type
+        if relative:
+            s.linkname = (
+                "../../traversal.txt"
+                if link_type == tarfile.SYMTYPE
+                else "../traversal.txt"
+            )
+        else:
+            s.linkname = target.as_posix()
+        tar.addfile(s)
+        p = b"path traversal"
+        f = tarfile.TarInfo("sub/link")
+        f.size = len(p)
+        tar.addfile(f, io.BytesIO(p))
+
+    exceptions: tuple[type[Exception], ...]
+    if hasattr(tarfile, "data_filter"):
+        exceptions = (
+            tarfile.AbsoluteLinkError,
+            tarfile.LinkOutsideDestinationError,
+            ValueError,
+        )
+    else:
+        # tarfile.OutsideDestinationError does not exist
+        exceptions = (ValueError,)
+
+    with pytest.raises(exceptions):
+        extractall(source=archive, dest=dest, zip=False)
+
+    if existing:
+        assert target.exists()
+        assert target.read_text(encoding="utf-8") == "original"
+    else:
+        assert not target.exists()
+
+
[email protected]("existing", [False, True])
+def test_extractall_wheel_no_path_traversal(
+    tmp_path: Path, wheel_with_path_traversal: Path, existing: bool
+) -> None:
+    """see also test_no_path_traversal in test_wheel_installer.py"""
+    dest = tmp_path / "dest" / "dir"
+    dest.mkdir(parents=True)
+    target = tmp_path / "traversal.txt"
+    if existing:
+        target.write_text("original", encoding="utf-8")
+
+    extractall(source=wheel_with_path_traversal, dest=dest, zip=True)
+
+    if existing:
+        assert target.exists()
+        assert target.read_text(encoding="utf-8") == "original"
+    else:
+        assert not target.exists()
+
+    # target is "../.." but also check ".." just to be sure
+    assert not (dest.parent / "traversal.txt").exists()
+
+
[email protected]("existing", [False, True])
+def test_extractall_wheel_no_path_traversal_via_symlink(
+    tmp_path: Path, wheel_with_path_traversal_via_symlink: Path, existing: bool
+) -> None:
+    """see also test_no_path_traversal_via_symlink in test_wheel_installer.py"""
+    dest = tmp_path / "dest" / "dir"
+    dest.mkdir(parents=True)
+    target_dir = tmp_path / "target"
+    target_dir.mkdir()
+    target = target_dir / "traversal.txt"
+    if existing:
+        target.write_text("original", encoding="utf-8")
+
+    with pytest.raises(FileNotFoundError if WINDOWS else NotADirectoryError):
+        extractall(source=wheel_with_path_traversal_via_symlink, dest=dest, zip=True)
+
+    assert target_dir.exists()
+    if existing:
+        assert target.exists()
+        assert target.read_text(encoding="utf-8") == "original"
+    else:
+        assert not target.exists()
diff -Nru poetry-2.3.2+dfsg/tests/vcs/git/test_backend.py poetry-2.3.4/tests/vcs/git/test_backend.py
--- poetry-2.3.2+dfsg/tests/vcs/git/test_backend.py	2026-02-01 17:31:39.000000000 +0200
+++ poetry-2.3.4/tests/vcs/git/test_backend.py	2026-04-12 18:09:54.000000000 +0300
@@ -8,6 +8,8 @@
 import pytest
 
 from dulwich.client import FetchPackResult
+from dulwich.refs import HEADREF
+from dulwich.refs import Ref
 from dulwich.repo import Repo
 
 from poetry.console.exceptions import PoetryRuntimeError
@@ -290,3 +292,135 @@
         f"Try again later or remove the {tag_ref_lock} manually"
         " if you are sure no other process is holding it."
     )
+
+
[email protected]_git_mock
+def test_clone_annotated_tag(tmp_path: Path) -> None:
+    """Test cloning at an annotated tag (issue #10658)."""
+    from dulwich import porcelain
+    from dulwich.objects import Commit
+
+    # Create a source repository with an annotated tag
+    source_path = tmp_path / "source-repo"
+    source_path.mkdir()
+    repo = Repo.init(str(source_path))
+
+    # Create initial commit
+    test_file = source_path / "test.txt"
+    test_file.write_text("test content", encoding="utf-8")
+    porcelain.add(repo, str(test_file))
+    expected_commit_sha = porcelain.commit(
+        repo,
+        message=b"Initial commit",
+        author=b"Test <[email protected]>",
+        committer=b"Test <[email protected]>",
+    )
+
+    # Create an annotated tag
+    porcelain.tag_create(
+        repo,
+        tag=b"v1.0.0",
+        message=b"Release 1.0.0",
+        author=b"Test <[email protected]>",
+        annotated=True,
+    )
+
+    # Clone at the annotated tag
+    source_root_dir = tmp_path / "clone-root"
+    source_root_dir.mkdir()
+    cloned_repo = Git.clone(
+        url=source_path.as_uri(),
+        source_root=source_root_dir,
+        name="clone-test",
+        tag="v1.0.0",
+    )
+
+    # Verify HEAD points to a commit, not a tag object
+    head_sha = cloned_repo.refs[HEADREF]
+    head_obj = cloned_repo.object_store[head_sha]
+    assert isinstance(head_obj, Commit), (
+        f"HEAD should point to a Commit, got {type(head_obj).__name__}"
+    )
+    # Verify it's the correct commit
+    assert head_sha == expected_commit_sha, (
+        f"HEAD should point to the expected commit {expected_commit_sha.hex()}, "
+        f"got {head_sha.hex()}"
+    )
+
+    # Verify the clone succeeded and files are present
+    clone_dir = source_root_dir / "clone-test"
+    assert (clone_dir / ".git").is_dir()
+    assert (clone_dir / "test.txt").exists()
+    assert (clone_dir / "test.txt").read_text(encoding="utf-8") == "test content"
+
+
[email protected]_git_mock
+def test_clone_nested_annotated_tags(tmp_path: Path) -> None:
+    """Test cloning at a tag that points to another tag (nested tags)."""
+    from dulwich import porcelain
+    from dulwich.objects import Commit
+    from dulwich.objects import Tag
+
+    # Create a source repository with nested annotated tags
+    source_path = tmp_path / "source-repo"
+    source_path.mkdir()
+    repo = Repo.init(str(source_path))
+
+    # Create initial commit
+    test_file = source_path / "test.txt"
+    test_file.write_text("nested tag test", encoding="utf-8")
+    porcelain.add(repo, paths=[b"test.txt"])
+    commit_sha = porcelain.commit(
+        repo,
+        message=b"Initial commit",
+        committer=b"Test <[email protected]>",
+        author=b"Test <[email protected]>",
+    )
+
+    # Create first annotated tag pointing to the commit
+    tag1 = Tag()
+    tag1.name = b"v1.0.0"
+    tag1.object = (Commit, commit_sha)
+    tag1.message = b"First tag"
+    tag1.tag_time = 1234567890
+    tag1.tag_timezone = 0
+    tag1.tagger = b"Test <[email protected]>"
+    repo.object_store.add_object(tag1)
+    repo.refs[Ref(b"refs/tags/v1.0.0")] = tag1.id
+
+    # Create second annotated tag pointing to the first tag
+    tag2 = Tag()
+    tag2.name = b"v1.0.0-release"
+    tag2.object = (Tag, tag1.id)
+    tag2.message = b"Second tag (points to first tag)"
+    tag2.tag_time = 1234567891
+    tag2.tag_timezone = 0
+    tag2.tagger = b"Test <[email protected]>"
+    repo.object_store.add_object(tag2)
+    repo.refs[Ref(b"refs/tags/v1.0.0-release")] = tag2.id
+
+    # Clone at the nested tag
+    source_root_dir = tmp_path / "clone-root"
+    source_root_dir.mkdir()
+    cloned_repo = Git.clone(
+        url=source_path.as_uri(),
+        source_root=source_root_dir,
+        name="clone-test",
+        tag="v1.0.0-release",
+    )
+
+    # Verify HEAD points to a commit, not a tag object
+    head_sha = cloned_repo.refs[HEADREF]
+    head_obj = cloned_repo.object_store[head_sha]
+    assert isinstance(head_obj, Commit), (
+        f"HEAD should point to a Commit (peeling nested tags), got {type(head_obj).__name__}"
+    )
+
+    # Verify it's the correct commit
+    assert head_sha == commit_sha
+
+    # Verify the clone succeeded and files are present
+    clone_dir = source_root_dir / "clone-test"
+    assert (clone_dir / ".git").is_dir()
+    assert (clone_dir / "test.txt").exists()
+    assert (clone_dir / "test.txt").read_text(encoding="utf-8") == "nested tag test"

Reply via email to