{"id":"d63d60ac93f17574","repo":"python-poetry/poetry","slug":"attempting-to-write-path-outside-of-the-target-d","errorCode":null,"errorMessage":"Attempting to write {path} outside of the target directory\nTarget directory: {target_dir}\nTarget path: {target_path_str}","messagePattern":"Attempting to write (.+?) outside of the target directory\nTarget directory: (.+?)\nTarget path: (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"critical","filePath":"src/poetry/installation/wheel_installer.py","lineNumber":86,"sourceCode":"        # 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        #\n        # We operate on plain strings and only build a Path at the end because\n        # this method is called once per file in the wheel: pathlib operations\n        # (especially Path.is_relative_to(), which materializes Path.parents)\n        # add up to a significant overhead during installation.\n        target_dir = self._abspath_scheme_dir(scheme)\n        target_path_str = os.path.abspath(os.path.join(target_dir, path))\n\n        # We do not need os.path.normcase() for this comparison\n        # because both paths are built from target_dir.\n        if target_path_str != target_dir and not target_path_str.startswith(\n            target_dir + os.sep\n        ):\n            raise ValueError(\n                f\"Attempting to write {path} outside of the target directory\\n\"\n                f\"Target directory: {target_dir}\\n\"\n                f\"Target path: {target_path_str}\"\n            )\n\n        target_path = Path(target_path_str)\n\n        if target_path.exists():\n            # Contrary to the base library we don't raise an error here since it can\n            # break pkgutil-style and pkg_resource-style namespace packages.\n            logger.warning(f\"Installing {target_path} over existing file\")\n\n        parent_folder = target_path.parent\n        if not parent_folder.exists():\n            # Due to the parallel installation it can happen\n            # that two threads try to create the directory.\n            parent_folder.mkdir(parents=True, exist_ok=True)\n","sourceCodeStart":68,"sourceCodeEnd":104,"githubUrl":"https://github.com/python-poetry/poetry/blob/92b74dcfe348d0e01e14d40d6c1fa47a4ee04a54/src/poetry/installation/wheel_installer.py#L68-L104","documentation":"Raised by WheelDestination.write_to_fs at src/poetry/installation/wheel_installer.py:83-90 when an entry in a wheel would be written outside its target scheme directory. The check uses os.path.abspath on target_dir joined with the entry path and verifies the result still starts with target_dir + os.sep — defeating absolute paths and `..` traversal. This is a Zip-Slip / path-traversal security guard. ValueError.","triggerScenarios":"Installing a wheel that contains a member whose path is absolute (leading '/') or contains '..' segments that escape the scheme dir. The installer iterates every file in the wheel and writes via this method.","commonSituations":"A maliciously crafted or buggy wheel (rare on public PyPI thanks to upload validation, but possible from private indexes), or a wheel produced by a faulty custom build backend that emits bad RECORD paths.","solutions":["Do not install the offending wheel — treat it as untrusted; audit how it was produced.","Rebuild the wheel with a mainstream build backend (setuptools/hatchling) so RECORD paths are sane.","If you control the source, ensure no file path in the package is absolute or contains '..'."],"exampleFix":"# before: wheel contains an entry like '/etc/badfile' or '../../escape'\n$ poetry install\nValueError: Attempting to write ... outside of the target directory ...\n\n# fix: rebuild the wheel with correct (relative, in-package) paths\n$ python -m build","handlingStrategy":"try-catch","validationCode":"import os\n\ndef is_safe_member(target_dir: str, member_path: str) -> bool:\n    abs_target = os.path.abspath(target_dir)\n    abs_member = os.path.abspath(os.path.join(target_dir, member_path))\n    return abs_member == abs_target or abs_member.startswith(abs_target + os.sep)","typeGuard":null,"tryCatchPattern":"try:\n    install(wheel_source, wheel_destination, {...})\nexcept ValueError as e:\n    if 'outside of the target directory' in str(e):\n        # do NOT bypass; quarantine the wheel and report\n        raise SystemExit(f'Unsafe wheel rejected: {e}') from e\n    raise","preventionTips":["Treat this error as a security signal — never silence it; the wheel is untrusted.","Build wheels only with reputable backends; scan private-index wheels before install.","Audit RECORD paths of any third-party wheel before deployment."],"tags":["security","path-traversal","wheel","installation","zip-slip"],"analyzedSha":"92b74dcfe348d0e01e14d40d6c1fa47a4ee04a54","analyzedAt":"2026-08-04T20:33:34.072Z","schemaVersion":2}