{"record":{"id":"48f5dc5d72e71bd4","repo":"oraios/serena","slug":"unsafe-archive-member-member-name-path-escape","errorCode":null,"errorMessage":"Unsafe archive member '{member_name}': path escapes extraction directory","messagePattern":"Unsafe archive member '(.+?)': path escapes extraction directory","errorType":"exception","errorClass":"SolidLSPException","httpStatus":null,"severity":"critical","filePath":"src/solidlsp/ls_utils.py","lineNumber":601,"sourceCode":"        normalized_allowed_hosts = {host.lower() for host in allowed_hosts}\n        if hostname is None or hostname.lower() not in normalized_allowed_hosts:\n            raise SolidLSPException(\n                f\"Refusing to download from host '{hostname or '<unknown>'}'; allowed hosts: {sorted(normalized_allowed_hosts)}\"\n            )\n\n    @staticmethod\n    def _validate_extraction_path(member_name: str, target_path: str) -> str:\n        \"\"\"\n        Validates that an archive member stays within the extraction root and returns its destination path.\n        \"\"\"\n        normalized_parts = Path(member_name).parts\n        if any(part == \"..\" for part in normalized_parts):\n            raise SolidLSPException(f\"Unsafe archive member '{member_name}': path traversal is not allowed\")\n\n        absolute_target_path = os.path.abspath(target_path)\n        absolute_member_path = os.path.abspath(os.path.join(target_path, member_name))\n        if not (absolute_member_path.startswith(absolute_target_path + os.sep) or absolute_member_path == absolute_target_path):\n            raise SolidLSPException(f\"Unsafe archive member '{member_name}': path escapes extraction directory\")\n\n        return absolute_member_path\n\n    @staticmethod\n    def _extract_zip_archive(archive_path: str, target_path: str) -> None:\n        \"\"\"\n        Extracts a ZIP archive safely while preserving Unix permissions when available.\n        \"\"\"\n        with zipfile.ZipFile(archive_path, \"r\") as zip_ref:\n            for zip_info in zip_ref.infolist():\n                extracted_path = FileUtils._validate_extraction_path(zip_info.filename, target_path)\n\n                if zip_info.is_dir():\n                    os.makedirs(extracted_path, exist_ok=True)\n                    continue\n\n                os.makedirs(os.path.dirname(extracted_path), exist_ok=True)\n                with zip_ref.open(zip_info, \"r\") as source_file, open(extracted_path, \"wb\") as output_file:","sourceCodeStart":583,"sourceCodeEnd":619,"githubUrl":"https://github.com/oraios/serena/blob/7fcbca7e62555ec2287ddb2f083caee805848ea6/src/solidlsp/ls_utils.py#L583-L619","documentation":"Second guard in _validate_extraction_path: even without literal '..' parts, the resolved absolute destination must stay under the target extraction directory (prefix check). Catches traversal via absolute member names, symlinks-in-name tricks, or platform-specific separators.","triggerScenarios":"Extracting an archive whose member path resolves outside target_path after abspath/join — e.g. an absolute member name like '/etc/passwd' or one that normalizes to a sibling directory.","commonSituations":"Same as Zip-Slip: untrusted or poorly packaged archives downloaded during language-server setup.","solutions":["Use archives from trusted sources with purely relative member paths","Repackage the archive so all entries are relative and contained in a single root folder","If you need extraction to a different root, change target_path, not the member names"],"exampleFix":"// before\nZipInfo filename = \"/absolute/path/file\"\n// after\nZipInfo filename = \"pkg/file\"  # relative, contained","handlingStrategy":"validation","validationCode":"import os\nfrom pathlib import Path\ndef members_stay_in_root(names, target):\n    root = os.path.abspath(target)\n    return all(os.path.abspath(os.path.join(target, n)).startswith(root + os.sep) or\n               os.path.abspath(os.path.join(target, n)) == root for n in names)","typeGuard":"def member_is_contained(target: str, member: str) -> bool:\n    root = os.path.abspath(target)\n    dest = os.path.abspath(os.path.join(target, member))\n    return dest.startswith(root + os.sep) or dest == root","tryCatchPattern":"try:\n    download_and_extract_archive_verified(url, target, archive_type=\"tar.gz\")\nexcept SolidLSPException as e:\n    if \"path escapes extraction directory\" in str(e):\n        raise SecurityError(\"archive member escapes extraction root\") from e\n    raise","preventionTips":["Package archives with a single top-level folder and relative paths","Scan member names for absolute paths before extraction","Extract into a dedicated temp dir with restricted permissions"],"tags":["security","zip-slip","path-traversal","archive"],"backgroundTag":"zip-slip-path-traversal","analyzedSha":"7fcbca7e62555ec2287ddb2f083caee805848ea6","analyzedAt":"2026-08-29T00:04:09.619Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}