pypa/pip · error · ValueError
path outside destination: %r
Error message
path outside destination: %r
What it means
Raised as ValueError by distlib.util.unarchive's inner check_path when an archive member's resolved path escapes the destination directory (a path-traversal / Zip-Slip guard). The check computes os.path.abspath(os.path.join(dest_dir, member)) and verifies it still starts with dest_dir followed by os.sep; members like '../../etc/passwd' or absolute paths fail. The same check applies to symlink/hardlink targets via check_link, blocking extraction of members whose linkname resolves outside dest_dir.
Solutions
- Keep check=True (default) and treat the error as the archive being untrusted — do not extract it.
- Audit the archive members (zipfile.namelist / tarfile.getmembers) for '..' or absolute paths before extraction.
- Extract into a sandboxed/throwaway directory and validate contents before use.
- Do NOT pass check=False to silence it unless you fully trust the source and accept traversal risk.
Example fix
# before
unarchive('untrusted.tar.gz', '/opt/app')
# after (validate first)
import tarfile
with tarfile.open('untrusted.tar.gz') as tf:
bad = [m.name for m in tf.getmembers() if m.name.startswith('/') or '..' in m.name]
if bad:
raise ValueError('unsafe members: %r' % bad)
unarchive('untrusted.tar.gz', '/opt/app') Defensive patterns
Strategy: try-catch
Validate before calling
def safe_unarchive(path, dest):
import tarfile, zipfile
if path.endswith(('.zip', '.whl')):
names = zipfile.ZipFile(path).namelist()
else:
names = [m.name for m in tarfile.open(path).getmembers()]
base = os.path.abspath(dest)
for n in names:
if not os.path.abspath(os.path.join(base, n)).startswith(base + os.sep):
raise ValueError('unsafe member: %r' % n)
unarchive(path, dest) Type guard
def archive_members_safe(path, dest) -> bool:
# returns False if any member escapes dest
...
return True Try / catch
try:
unarchive(path, dest, check=True)
except ValueError as e:
if 'path outside destination' in str(e):
# archive is untrusted: do NOT disable check; quarantine instead
raise SecurityError('refusing unsafe archive: %s' % path) Prevention
- Never pass check=False for untrusted archives.
- Extract into sandboxed throwaway dirs.
- Audit members for '..' and absolute paths first.
When it happens
Trigger: unarchive('malicious.zip', '/tmp/out') where a member is named '../../etc/cron/evil'; extracting a wheel/tar with a symlink whose target points outside dest_dir; archive generated by a hostile or buggy packager with absolute member paths. Disable by passing check=False (NOT recommended — defeats the security guard).
Common situations: Processing untrusted third-party wheels/sdists; CI extracting artifacts from external sources; legacy tarballs using absolute paths; supply-chain attack mitigation.
Related errors
- The wheel has a file trying to install outside the target…
- The zip file ( ) has a file ( ) trying to install outside…
- Unknown format for %r
- Invalid member in the tar file
- Invalid script entry point name
AI-assisted analysis of pypa/pip@f399c37189 (2026-08-08).
Data as JSON: /api/errors/f6157a76f0be4c56.
Report an issue: GitHub.
Appendix: source
Thrown at src/pip/_vendor/distlib/util.py:1237
#
# Unarchiving functionality for zip, tar, tgz, tbz, whl
#
ARCHIVE_EXTENSIONS = ('.tar.gz', '.tar.bz2', '.tar', '.zip', '.tgz', '.tbz', '.whl')
def unarchive(archive_filename, dest_dir, format=None, check=True):
def check_path(path, base=None):
if not isinstance(path, text_type):
path = path.decode('utf-8')
if base is None:
base = dest_dir
p = os.path.abspath(os.path.join(base, path))
if not p.startswith(dest_dir) or p[plen] != os.sep:
raise ValueError('path outside destination: %r' % p)
def check_link(member):
# A symlink/hardlink member's name is validated like any other
# member, but its target (linkname) is not covered by extractall's
# name-based handling. An unchecked target lets a later member be
# written through the link to a location outside dest_dir. Validate
# the resolved target stays within dest_dir. Symlink targets are
# relative to the member's own directory; hardlink targets are
# relative to the archive root (i.e. dest_dir).
if not (member.issym() or member.islnk()):
return
if member.issym():
link_base = os.path.dirname(os.path.join(dest_dir, member.name))
else:
link_base = dest_dir
check_path(member.linkname, base=link_base)
dest_dir = os.path.abspath(dest_dir)View on GitHub (pinned to f399c37189)