pypa/pip · critical · ValueError
path outside destination: %r
Error message
path outside destination: %r
What it means
Raised by the check_path() helper inside unarchive() when an archive member's normalized absolute path does not stay within dest_dir (or a symlink/hardlink target escapes it). This is distlib's defense against the 'Zip Slip' / TarSlip path-traversal attack where a malicious archive contains '..' segments or absolute paths; extraction is aborted with ValueError 'path outside destination'.
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 d7d0d0a394)
Solutions
- Treat the error as a security signal: do NOT disable checking; investigate the archive contents with 'tar -tvf' / 'unzip -l'.
- Obtain the archive from a trusted source or rebuild it without path-traversal entries.
- If you must extract untrusted archives, extract to a throwaway sandbox directory and keep check=True.
Example fix
// before
unarchive('untrusted.tar.gz', '/opt/app') # contains ../../etc/cron.d/x
// after
# Do not extract; verify/rebuild archive first:
# tar -tvf untrusted.tar.gz | grep '\.\.'
# then use a trusted archive with the same call. Defensive patterns
Strategy: validation
Validate before calling
import os
def safe_unarchive(archive, dest, **kw):
# pre-scan members without extracting
import tarfile, zipfile
dest = os.path.abspath(dest)
if archive.endswith(('.tar.gz','.tgz','.tar.bz2','.tbz','.tar')):
with tarfile.open(archive) as tf:
for m in tf.getmembers():
if not os.path.abspath(os.path.join(dest, m.name)).startswith(dest + os.sep):
raise ValueError('unsafe member: %r' % m.name)
elif archive.endswith('.zip'):
with zipfile.ZipFile(archive) as zf:
for n in zf.namelist():
if not os.path.abspath(os.path.join(dest, n)).startswith(dest + os.sep):
raise ValueError('unsafe member: %r' % n)
from distlib.util import unarchive
return unarchive(archive, dest, **kw) Try / catch
from distlib.util import unarchive
try:
unarchive(path, dest) # check=True default
except ValueError as e:
if 'path outside destination' in str(e):
quarantine(path) # treat as malicious; do NOT pass check=False
else:
raise Prevention
- Never disable check=True to 'get past' this error; treat it as a security alert.
- Only extract archives from trusted, integrity-checked sources.
- Pre-scan archive members for '..' or absolute paths before extraction.
When it happens
Trigger: unarchive() on a tar/zip containing an entry like '../../../etc/passwd' or '/etc/cron.d/x', or a member whose symlink linkname resolves outside dest_dir. Triggered only when check=True (the default).
Common situations: Extracting untrusted or third-party sdists/wheels, corrupted archives, or archives produced by tools that emit absolute member names.
Related errors
- The wheel {wheel_path!r} has a file {target_path!r} trying t
- The zip file ({}) has a file ({}) trying to install outside
- Invalid member in the tar file {}: {}
- The tar file ({}) has a file ({}) trying to install outside
- Invalid script entry point name {entry.name!r}: the script w
AI-assisted analysis of pypa/pip@d7d0d0a394 (2026-08-04).
Data as JSON: /data/errors/f6157a76f0be4c56.json.
Report an issue: GitHub.