{"id":"f6157a76f0be4c56","repo":"pypa/pip","slug":"path-outside-destination-r","errorCode":null,"errorMessage":"path outside destination: %r","messagePattern":"path outside destination: %r","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"critical","filePath":"src/pip/_vendor/distlib/util.py","lineNumber":1237,"sourceCode":"\n\n#\n# Unarchiving functionality for zip, tar, tgz, tbz, whl\n#\n\nARCHIVE_EXTENSIONS = ('.tar.gz', '.tar.bz2', '.tar', '.zip', '.tgz', '.tbz', '.whl')\n\n\ndef unarchive(archive_filename, dest_dir, format=None, check=True):\n\n    def check_path(path, base=None):\n        if not isinstance(path, text_type):\n            path = path.decode('utf-8')\n        if base is None:\n            base = dest_dir\n        p = os.path.abspath(os.path.join(base, path))\n        if not p.startswith(dest_dir) or p[plen] != os.sep:\n            raise ValueError('path outside destination: %r' % p)\n\n    def check_link(member):\n        # A symlink/hardlink member's name is validated like any other\n        # member, but its target (linkname) is not covered by extractall's\n        # name-based handling. An unchecked target lets a later member be\n        # written through the link to a location outside dest_dir. Validate\n        # the resolved target stays within dest_dir. Symlink targets are\n        # relative to the member's own directory; hardlink targets are\n        # relative to the archive root (i.e. dest_dir).\n        if not (member.issym() or member.islnk()):\n            return\n        if member.issym():\n            link_base = os.path.dirname(os.path.join(dest_dir, member.name))\n        else:\n            link_base = dest_dir\n        check_path(member.linkname, base=link_base)\n\n    dest_dir = os.path.abspath(dest_dir)","sourceCodeStart":1219,"sourceCodeEnd":1255,"githubUrl":"https://github.com/pypa/pip/blob/d7d0d0a39494e28ec1c407bd0680e4a4d1067791/src/pip/_vendor/distlib/util.py#L1219-L1255","documentation":"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'.","triggerScenarios":"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).","commonSituations":"Extracting untrusted or third-party sdists/wheels, corrupted archives, or archives produced by tools that emit absolute member names.","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."],"exampleFix":"// before\nunarchive('untrusted.tar.gz', '/opt/app')  # contains ../../etc/cron.d/x\n// after\n# Do not extract; verify/rebuild archive first:\n#   tar -tvf untrusted.tar.gz | grep '\\.\\.'\n# then use a trusted archive with the same call.","handlingStrategy":"validation","validationCode":"import os\ndef safe_unarchive(archive, dest, **kw):\n    # pre-scan members without extracting\n    import tarfile, zipfile\n    dest = os.path.abspath(dest)\n    if archive.endswith(('.tar.gz','.tgz','.tar.bz2','.tbz','.tar')):\n        with tarfile.open(archive) as tf:\n            for m in tf.getmembers():\n                if not os.path.abspath(os.path.join(dest, m.name)).startswith(dest + os.sep):\n                    raise ValueError('unsafe member: %r' % m.name)\n    elif archive.endswith('.zip'):\n        with zipfile.ZipFile(archive) as zf:\n            for n in zf.namelist():\n                if not os.path.abspath(os.path.join(dest, n)).startswith(dest + os.sep):\n                    raise ValueError('unsafe member: %r' % n)\n    from distlib.util import unarchive\n    return unarchive(archive, dest, **kw)","typeGuard":null,"tryCatchPattern":"from distlib.util import unarchive\ntry:\n    unarchive(path, dest)  # check=True default\nexcept ValueError as e:\n    if 'path outside destination' in str(e):\n        quarantine(path)  # treat as malicious; do NOT pass check=False\n    else:\n        raise","preventionTips":["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."],"tags":["security","archive","path-traversal","zip-slip","tar","validation"],"analyzedSha":"d7d0d0a39494e28ec1c407bd0680e4a4d1067791","analyzedAt":"2026-08-04T20:55:04.259Z","schemaVersion":2}