pypa/pip · critical · InstallationError

The wheel {wheel_path!r} has a file {target_path!r} trying t

Error message

The wheel {wheel_path!r} has a file {target_path!r} trying to install outside the target directory {dest_dir_path!r}

What it means

Raised as InstallationError by assert_no_path_traversal() (wheel.py:489-497) when a file inside a wheel resolves to a destination outside its target directory. The check uses is_within_directory(dest_dir_path, target_path) for every root-scheme and data-scheme file before it is written, refusing to write anything that escapes the install prefix.

Source

Thrown at src/pip/_internal/operations/install/wheel.py:495

    def record_installed(
        srcfile: RecordPath, destfile: str, modified: bool = False
    ) -> None:
        """Map archive RECORD paths to installation RECORD paths."""
        newpath = _fs_to_record_path(destfile, lib_dir)
        installed[srcfile] = newpath
        if modified:
            changed.add(newpath)

    def is_dir_path(path: RecordPath) -> bool:
        return path.endswith("/")

    def assert_no_path_traversal(dest_dir_path: str, target_path: str) -> None:
        if not is_within_directory(dest_dir_path, target_path):
            message = (
                "The wheel {!r} has a file {!r} trying to install"
                " outside the target directory {!r}"
            )
            raise InstallationError(
                message.format(wheel_path, target_path, dest_dir_path)
            )

    def root_scheme_file_maker(
        zip_file: ZipFile, dest: str
    ) -> Callable[[RecordPath], File]:
        def make_root_scheme_file(record_path: RecordPath) -> File:
            normed_path = os.path.normpath(record_path)
            dest_path = os.path.join(dest, normed_path)
            assert_no_path_traversal(dest, dest_path)
            return ZipBackedFile(record_path, dest_path, zip_file)

        return make_root_scheme_file

    def data_scheme_file_maker(
        zip_file: ZipFile, scheme: Scheme
    ) -> Callable[[RecordPath], File]:
        scheme_paths = {key: getattr(scheme, key) for key in SCHEME_KEYS}

View on GitHub (pinned to d7d0d0a394)

Solutions

  1. List the wheel contents and look for absolute or parent-traversal paths: python -c "import zipfile; zipfile.ZipFile('pkg.whl').printdir()".
  2. Do not install the offending wheel; report it to upstream maintainers.
  3. Pin to a known-good version of the package that has clean record paths.
  4. If you built the wheel yourself, fix the build to emit only paths relative to the scheme root.

Example fix

# before (wheel RECORD contains)
../../etc/cron.d/payload

# after
payload/__init__.py
Defensive patterns

Strategy: validation

Validate before calling

import zipfile, os

def validate_wheel_paths(whl):
    with zipfile.ZipFile(whl) as z:
        for info in z.infolist():
            norm = os.path.normpath(info.filename)
            if os.path.isabs(norm) or norm.startswith("..") or f"{os.sep}.." in norm:
                raise ValueError(f"wheel {whl} contains unsafe path: {info.filename}")

Type guard

import os
def path_is_within(path: str, base: str) -> bool:
    ap = os.path.abspath(path)
    ab = os.path.abspath(base)
    return ap == ab or ap.startswith(ab + os.sep)

Prevention

When it happens

Trigger: A wheel archive contains a member whose record path (after os.path.normpath and os.path.join) resolves above the target lib/scripts/include directory, e.g. '../../etc/cron.d/evil' or an absolute path. Encountered via root_scheme_file_maker or data_scheme_file_maker.

Common situations: A malicious or misbuilt wheel (Zip Slip vulnerability). Common in security audits of third-party packages. Rare in well-formed wheels built by modern setuptools.

Related errors


AI-assisted analysis of pypa/pip@d7d0d0a394 (2026-08-04). Data as JSON: /data/errors/5e3c941d82282d43.json. Report an issue: GitHub.