pypa/pip · error · InstallationError

The wheel {!r} has a file {!r} trying to install outside the

Error message

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

What it means

Raised as InstallationError by assert_no_path_traversal when a file extracted from a wheel resolves, after os.path.normpath/join, to a destination outside its target install directory. This blocks zip-slip / path-traversal attacks where archive entries use '..' or absolute paths to write outside the scheme directory.

Source

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

    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 f399c37189)

Solutions

  1. Do not install the offending wheel; treat it as untrusted.
  2. Rebuild the wheel with a standard build backend (setuptools/hatchling/flit/wheel) that emits sane paths.
  3. Report the issue to the wheel author/maintainer.
  4. Verify the wheel's hash/signature against the publisher.
Defensive patterns

Strategy: validation

Validate before calling

import os

def is_within_directory(directory: str, target: str) -> bool:
    abs_dir = os.path.realpath(directory)
    abs_tgt = os.path.realpath(target)
    return os.path.commonpath([abs_dir]) == os.path.commonpath([abs_dir, abs_tgt])

def wheel_paths_safe(record_paths, target: str) -> bool:
    return all(is_within_directory(target, os.path.join(target, p)) for p in record_paths)

Prevention

When it happens

Trigger: The wheel archive contains a RECORD path whose normalized destination escapes the lib_dir or scheme path; is_within_directory(dest_dir_path, target_path) is false for root-scheme or data-scheme files.

Common situations: Malicious or corrupted wheel; buggy wheel-building tool that emitted absolute or parent-referencing paths; tampered artefact from an untrusted index.

Related errors


AI-assisted analysis of pypa/pip@f399c37189 (2026-08-08). Data as JSON: /api/errors/b92d637c23de0135. Report an issue: GitHub.