{"record":{"id":"f6157a76f0be4c56","repo":"pypa/pip","slug":"path-outside-destination-r","errorCode":null,"errorMessage":"path outside destination: %r","messagePattern":"path outside destination: %r","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","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/f399c3718970b1b0e2478dac5296eb62679a9b86/src/pip/_vendor/distlib/util.py#L1219-L1255","documentation":"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.","triggerScenarios":"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).","commonSituations":"Processing untrusted third-party wheels/sdists; CI extracting artifacts from external sources; legacy tarballs using absolute paths; supply-chain attack mitigation.","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."],"exampleFix":"# before\nunarchive('untrusted.tar.gz', '/opt/app')\n# after (validate first)\nimport tarfile\nwith tarfile.open('untrusted.tar.gz') as tf:\n    bad = [m.name for m in tf.getmembers() if m.name.startswith('/') or '..' in m.name]\nif bad:\n    raise ValueError('unsafe members: %r' % bad)\nunarchive('untrusted.tar.gz', '/opt/app')","handlingStrategy":"try-catch","validationCode":"def safe_unarchive(path, dest):\n    import tarfile, zipfile\n    if path.endswith(('.zip', '.whl')):\n        names = zipfile.ZipFile(path).namelist()\n    else:\n        names = [m.name for m in tarfile.open(path).getmembers()]\n    base = os.path.abspath(dest)\n    for n in names:\n        if not os.path.abspath(os.path.join(base, n)).startswith(base + os.sep):\n            raise ValueError('unsafe member: %r' % n)\n    unarchive(path, dest)","typeGuard":"def archive_members_safe(path, dest) -> bool:\n    # returns False if any member escapes dest\n    ...\n    return True","tryCatchPattern":"try:\n    unarchive(path, dest, check=True)\nexcept ValueError as e:\n    if 'path outside destination' in str(e):\n        # archive is untrusted: do NOT disable check; quarantine instead\n        raise SecurityError('refusing unsafe archive: %s' % path)","preventionTips":["Never pass check=False for untrusted archives.","Extract into sandboxed throwaway dirs.","Audit members for '..' and absolute paths first."],"tags":["security","path-traversal","zip-slip","distlib","archive","unarchive"],"backgroundTag":null,"analyzedSha":"f399c3718970b1b0e2478dac5296eb62679a9b86","analyzedAt":"2026-08-08T23:01:42.227Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}