{"id":"f85dd7a9615a2f2a","repo":"python-poetry/poetry","slug":"refusing-to-extract-member-name-would-write-out","errorCode":null,"errorMessage":"Refusing to extract {member.name}: would write outside {dest}","messagePattern":"Refusing to extract (.+?): would write outside (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"src/poetry/utils/helpers.py","lineNumber":310,"sourceCode":"            if (\n                hasattr(tarfile, \"data_filter\")\n                and sys.version_info[:3] not in broken_tarfile_filter\n            ):\n                archive.extractall(dest, filter=\"data\")\n            else:\n                # Validate all member paths before extraction\n                #\n                # Attention: Path.absolute() is not sufficient because it does not\n                #  normalize, i.e. does not remove \"..\".\n                #\n                # We want to avoid Path.resolve() because it is significantly slower\n                # than os.path.abspath()!\n                dest = Path(os.path.abspath(dest))\n                safe_members = []\n                for member in archive.getmembers():\n                    member_path = Path(os.path.abspath(dest / member.name))\n                    if not member_path.is_relative_to(dest):\n                        raise ValueError(\n                            f\"Refusing to extract {member.name}: \"\n                            f\"would write outside {dest}\"\n                        )\n                    if member.issym():\n                        link_target = Path(\n                            os.path.abspath(member_path.parent / member.linkname)\n                        )\n                        if not link_target.is_relative_to(dest):\n                            raise ValueError(\n                                f\"Refusing symlink {member.name}: \"\n                                f\"target {member.linkname} outside {dest}\"\n                            )\n                    elif member.islnk():\n                        link_target = Path(os.path.abspath(dest / member.linkname))\n                        if not link_target.is_relative_to(dest):\n                            raise ValueError(\n                                f\"Refusing hardlink {member.name}: \"\n                                f\"target {member.linkname} outside {dest}\"","sourceCodeStart":292,"sourceCodeEnd":328,"githubUrl":"https://github.com/python-poetry/poetry/blob/92b74dcfe348d0e01e14d40d6c1fa47a4ee04a54/src/poetry/utils/helpers.py#L292-L328","documentation":"Raised by poetry.utils.helpers.extractall (zip=False path) when extracting a tar archive on a Python build that lacks a functional tarfile.data_filter (Python without the attr, or the broken 3.10.12 / 3.11.4 patch levels). For each tar member it computes the absolute destination path and rejects any member whose resolved path is not contained under dest. This is an explicit Zip-Slip / path-traversal guard that mirrors what CPython's data_filter would otherwise enforce.","triggerScenarios":"Calling poetry.utils.helpers.extractall(source, dest, zip=False) where the tar archive contains a member name that resolves outside dest, e.g. '../../etc/passwd', an absolute '/etc/...', or any name using '..' segments, while running on a Python whose tarfile has no (working) data_filter.","commonSituations":"A crafted or compromised sdist tarball recorded with absolute paths; archives produced by tooling that preserve leading slashes or '..'; CI runners pinned to Python 3.10.12 or 3.11.4 (the two versions the code blocklists as having a broken data_filter).","solutions":["Inspect the archive with `tar -tvf <file>` and identify members with leading '/', absolute paths, or '..' segments, then rebuild or re-download a clean archive.","Run on a Python version that ships a working tarfile.data_filter (>=3.10.13, >=3.11.5, or >=3.12) so extractall uses archive.extractall(dest, filter='data') instead of the manual check.","If the archive is third-party and looks malicious, discard it and re-acquire it from a trusted source.","Patch the producing tool so it writes member names relative to the archive root."],"exampleFix":"# before\nextractall(Path('untrusted.tar'), Path('/app/out'), zip=False)\n# after - reject pathological members first\nimport tarfile, os\nfrom pathlib import Path\nwith tarfile.open('untrusted.tar') as a:\n    dest = Path(os.path.abspath('/app/out'))\n    for m in a.getmembers():\n        if not Path(os.path.abspath(dest / m.name)).is_relative_to(dest):\n            raise ValueError(f'unsafe member {m.name}')\n    a.extractall(dest)","handlingStrategy":"validation","validationCode":"import os, tarfile\nfrom pathlib import Path\n\ndef is_safe_tar(source: Path, dest: Path) -> bool:\n    dest = Path(os.path.abspath(dest))\n    with tarfile.open(source) as a:\n        for m in a.getmembers():\n            if not Path(os.path.abspath(dest / m.name)).is_relative_to(dest):\n                return False\n    return True\n\n# call before extractall(..., zip=False)\nassert is_safe_tar(source, dest)","typeGuard":null,"tryCatchPattern":"from poetry.utils.helpers import extractall\ntry:\n    extractall(source, dest, zip=False)\nexcept ValueError as e:\n    # path-traversal member; refuse to proceed\n    raise","preventionTips":["Only extract archives from trusted publishers.","Run on a Python version with a functional tarfile.data_filter so filter='data' is used.","Extract into a disposable directory/container and inspect members first."],"tags":["security","tar","path-traversal","archive","extraction"],"analyzedSha":"92b74dcfe348d0e01e14d40d6c1fa47a4ee04a54","analyzedAt":"2026-08-04T20:33:34.072Z","schemaVersion":2}