python-poetry/poetry · critical · ValueError

Attempting to write {path} outside of the target directory T

Error message

Attempting to write {path} outside of the target directory
Target directory: {target_dir}
Target path: {target_path_str}

What it means

Raised by WheelDestination.write_to_fs at src/poetry/installation/wheel_installer.py:83-90 when an entry in a wheel would be written outside its target scheme directory. The check uses os.path.abspath on target_dir joined with the entry path and verifies the result still starts with target_dir + os.sep — defeating absolute paths and `..` traversal. This is a Zip-Slip / path-traversal security guard. ValueError.

Source

Thrown at src/poetry/installation/wheel_installer.py:86

        # 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()!
        #
        # We operate on plain strings and only build a Path at the end because
        # this method is called once per file in the wheel: pathlib operations
        # (especially Path.is_relative_to(), which materializes Path.parents)
        # add up to a significant overhead during installation.
        target_dir = self._abspath_scheme_dir(scheme)
        target_path_str = os.path.abspath(os.path.join(target_dir, path))

        # We do not need os.path.normcase() for this comparison
        # because both paths are built from target_dir.
        if target_path_str != target_dir and not target_path_str.startswith(
            target_dir + os.sep
        ):
            raise ValueError(
                f"Attempting to write {path} outside of the target directory\n"
                f"Target directory: {target_dir}\n"
                f"Target path: {target_path_str}"
            )

        target_path = Path(target_path_str)

        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.
            logger.warning(f"Installing {target_path} over existing file")

        parent_folder = target_path.parent
        if not parent_folder.exists():
            # Due to the parallel installation it can happen
            # that two threads try to create the directory.
            parent_folder.mkdir(parents=True, exist_ok=True)

View on GitHub (pinned to 92b74dcfe3)

Solutions

  1. Do not install the offending wheel — treat it as untrusted; audit how it was produced.
  2. Rebuild the wheel with a mainstream build backend (setuptools/hatchling) so RECORD paths are sane.
  3. If you control the source, ensure no file path in the package is absolute or contains '..'.

Example fix

# before: wheel contains an entry like '/etc/badfile' or '../../escape'
$ poetry install
ValueError: Attempting to write ... outside of the target directory ...

# fix: rebuild the wheel with correct (relative, in-package) paths
$ python -m build
Defensive patterns

Strategy: try-catch

Validate before calling

import os

def is_safe_member(target_dir: str, member_path: str) -> bool:
    abs_target = os.path.abspath(target_dir)
    abs_member = os.path.abspath(os.path.join(target_dir, member_path))
    return abs_member == abs_target or abs_member.startswith(abs_target + os.sep)

Try / catch

try:
    install(wheel_source, wheel_destination, {...})
except ValueError as e:
    if 'outside of the target directory' in str(e):
        # do NOT bypass; quarantine the wheel and report
        raise SystemExit(f'Unsafe wheel rejected: {e}') from e
    raise

Prevention

When it happens

Trigger: Installing a wheel that contains a member whose path is absolute (leading '/') or contains '..' segments that escape the scheme dir. The installer iterates every file in the wheel and writes via this method.

Common situations: A maliciously crafted or buggy wheel (rare on public PyPI thanks to upload validation, but possible from private indexes), or a wheel produced by a faulty custom build backend that emits bad RECORD paths.

Related errors


AI-assisted analysis of python-poetry/poetry@92b74dcfe3 (2026-08-04). Data as JSON: /data/errors/d63d60ac93f17574.json. Report an issue: GitHub.