pypa/pip · error · InstallationError

Path in pylock file obtained from a URL resolves outside…

Error message

Path {path!r} in pylock file obtained from a URL resolves outside its location: {pylock_path_or_url!r}

What it means

Raised as InstallationError by _package_dist_url (pylock.py:167) when a relative path in a pylock file that was loaded from a URL resolves to a different scheme or host than the pylock file itself. This is a path-traversal guard: when a remote pylock.toml contains a relative `path` field, pip joins it with the pylock's base URL; if the result's scheme/netloc differs (e.g. a `../` escape or `//evil.com` redirect), pip refuses to follow it to prevent fetching files from arbitrary locations.

Solutions

  1. Ensure all relative paths in the pylock file resolve under the same scheme and host as the pylock URL.
  2. Replace relative paths with absolute URLs in the lock file's url field for each package.
  3. Host the pylock file locally (file:// or a local path) so relative paths resolve on the filesystem instead of being URL-joined.
  4. Regenerate the pylock file from a trusted source (e.g. `pip lock` / pip-tools) so paths are consistent.

Example fix

// before (pylock at https://index.example.com/locks/pylock.toml)
[[packages]]
name = "pkg"
path = "../../../other-host/pkg.whl"

// after
[[packages]]
name = "pkg"
url = "https://index.example.com/wheels/pkg.whl"
Defensive patterns

Strategy: validation

Validate before calling

from urllib.parse import urljoin, urlsplit

def validate_pylock_relative_path(pylock_url: str, rel_path: str) -> bool:
    """True if a relative path in a remote pylock stays on the same scheme/host."""
    resolved = urljoin(pylock_url, rel_path)
    base = urlsplit(pylock_url)
    target = urlsplit(resolved)
    return (target.scheme, target.netloc) == (base.scheme, base.netloc)

Type guard

from urllib.parse import urljoin, urlsplit

def is_safe_pylock_path(pylock_url: str, path: str) -> bool:
    """True if the path resolves within the pylock URL's origin."""
    resolved = urljoin(pylock_url, path)
    b, t = urlsplit(pylock_url), urlsplit(resolved)
    return t.scheme == b.scheme and t.netloc == b.netloc

Prevention

When it happens

Trigger: Loading a pylock file from an http(s):// URL whose package entry has a relative path that, after urljoin with the base, changes the scheme or netloc. The check at lines 164-166 compares urlsplit results; mismatch raises at 167.

Common situations: A pylock file hosted on a CDN or index whose package paths use ../../ or //other-host/ patterns. A lock file generated by a tool that emitted paths relative to a different base. Malicious or misconfigured lock files that attempt to redirect downloads.

Related errors


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

Appendix: source

Thrown at src/pip/_internal/utils/pylock.py:167

) -> str:
    """Compute an url from a Pylock package path and url.

    Give priority to path over url. If path is relative,
    compute an url using the pylock file location as base.
    """
    if path is not None:
        if not os.path.isabs(path):
            # relative path, join to pylock location
            if _is_url(pylock_path_or_url):
                dist_url = urljoin(pylock_path_or_url, path)
                # os.path.isabs does not treat a scheme-carrying value like
                # "file:..." as absolute, so it reaches here and urljoin honors
                # its scheme, discarding the pylock base. Only keep the result
                # if its scheme and host still match the lock's own.
                base = urlsplit(pylock_path_or_url)
                target = urlsplit(dist_url)
                if (target.scheme, target.netloc) != (base.scheme, base.netloc):
                    raise InstallationError(
                        f"Path {path!r} in pylock file obtained from a URL "
                        f"resolves outside its location: {pylock_path_or_url!r}"
                    )
                return dist_url
            else:
                return path_to_url(
                    os.path.join(os.path.dirname(pylock_path_or_url), path)
                )
        else:
            # absolute path, reject if pylock comes from a URL
            if _is_url(pylock_path_or_url):
                raise InstallationError(
                    f"Absolute paths are not supported in pylock files obtained "
                    f"from a URL: {path!r} in {pylock_path_or_url!r}"
                )
            return path_to_url(path)
    else:
        assert url is not None  # guaranteed by packaging.pylock validation

View on GitHub (pinned to f399c37189)