{"id":"fb760a383087b53f","repo":"python-poetry/poetry","slug":"refusing-symlink-member-name-target-member-lin","errorCode":null,"errorMessage":"Refusing symlink {member.name}: target {member.linkname} outside {dest}","messagePattern":"Refusing symlink (.+?): target (.+?) outside (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"src/poetry/utils/helpers.py","lineNumber":319,"sourceCode":"                #  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}\"\n                            )\n                    safe_members.append(member)\n                archive.extractall(dest, members=safe_members)\n\n\n_DEPRECATED_DOWNLOAD_EXPORTS = {\n    \"Downloader\",\n    \"download_file\",\n    \"HTTPRangeRequestSupportedError\",","sourceCodeStart":301,"sourceCodeEnd":337,"githubUrl":"https://github.com/python-poetry/poetry/blob/92b74dcfe348d0e01e14d40d6c1fa47a4ee04a54/src/poetry/utils/helpers.py#L301-L337","documentation":"Raised by extractall during the manual (non-data_filter) tar validation, specifically for symbolic-link members (member.issym()). The link target is resolved relative to the member's parent directory and, if it falls outside dest, extraction is refused. This blocks symlink-based traversal where a symlink inside the archive points at a file outside the extraction directory.","triggerScenarios":"Extracting a tar (zip=False, on a Python without a working data_filter) that contains a symlink member whose linkname resolves outside dest, e.g. a symlink 'run -> ../../../etc/passwd' or 'link -> /root/.ssh/id_rsa'.","commonSituations":"Malicious sdists planting symlinks to harvest host secrets; legitimately-bundled symlinks that assume a different install layout; CI on blocklisted Python patch versions.","solutions":["Run `tar -tvf <file>` and look for symlink entries ('l'/'s' type flags) whose targets escape the archive root, then rebuild the archive without them.","Move to a Python with a working tarfile.data_filter so the validated filter='data' path is used.","Re-download the artifact from a trusted publisher if the symlinks look hostile.","Extract into a throwaway directory/container to confirm safety first."],"exampleFix":"# before\nextractall(Path('pkg.tar'), dest, zip=False)\n# after - vet symlink targets before extracting\nimport tarfile, os\nfrom pathlib import Path\nwith tarfile.open('pkg.tar') as a:\n    dest = Path(os.path.abspath(dest))\n    for m in a.getmembers():\n        if m.issym():\n            t = Path(os.path.abspath((dest / m.name).parent / m.linkname))\n            if not t.is_relative_to(dest):\n                raise ValueError(f'symlink escapes dest: {m.name}')\n    a.extractall(dest)","handlingStrategy":"validation","validationCode":"import os, tarfile\nfrom pathlib import Path\n\ndef tar_symlinks_safe(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 m.issym():\n                t = Path(os.path.abspath((dest / m.name).parent / m.linkname))\n                if not t.is_relative_to(dest):\n                    return False\n    return True","typeGuard":null,"tryCatchPattern":"try:\n    extractall(source, dest, zip=False)\nexcept ValueError as e:\n    # symlink escaped dest; do not extract\n    raise","preventionTips":["Inspect symlink entries with `tar -tvf` before extracting.","Use a Python with a working data_filter to delegate filtering to CPython.","Extract untrusted archives in an isolated sandbox."],"tags":["security","tar","symlink","path-traversal","archive"],"analyzedSha":"92b74dcfe348d0e01e14d40d6c1fa47a4ee04a54","analyzedAt":"2026-08-04T20:33:34.072Z","schemaVersion":2}