oraios/serena · critical · SolidLSPException
Unsafe archive member '{member_name}': path escapes extracti
Error message
Unsafe archive member '{member_name}': path escapes extraction directory What it means
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.
Source
Thrown at src/solidlsp/ls_utils.py:601
normalized_allowed_hosts = {host.lower() for host in allowed_hosts}
if hostname is None or hostname.lower() not in normalized_allowed_hosts:
raise SolidLSPException(
f"Refusing to download from host '{hostname or '<unknown>'}'; allowed hosts: {sorted(normalized_allowed_hosts)}"
)
@staticmethod
def _validate_extraction_path(member_name: str, target_path: str) -> str:
"""
Validates that an archive member stays within the extraction root and returns its destination path.
"""
normalized_parts = Path(member_name).parts
if any(part == ".." for part in normalized_parts):
raise SolidLSPException(f"Unsafe archive member '{member_name}': path traversal is not allowed")
absolute_target_path = os.path.abspath(target_path)
absolute_member_path = os.path.abspath(os.path.join(target_path, member_name))
if not (absolute_member_path.startswith(absolute_target_path + os.sep) or absolute_member_path == absolute_target_path):
raise SolidLSPException(f"Unsafe archive member '{member_name}': path escapes extraction directory")
return absolute_member_path
@staticmethod
def _extract_zip_archive(archive_path: str, target_path: str) -> None:
"""
Extracts a ZIP archive safely while preserving Unix permissions when available.
"""
with zipfile.ZipFile(archive_path, "r") as zip_ref:
for zip_info in zip_ref.infolist():
extracted_path = FileUtils._validate_extraction_path(zip_info.filename, target_path)
if zip_info.is_dir():
os.makedirs(extracted_path, exist_ok=True)
continue
os.makedirs(os.path.dirname(extracted_path), exist_ok=True)
with zip_ref.open(zip_info, "r") as source_file, open(extracted_path, "wb") as output_file:View on GitHub (pinned to 7fcbca7e62)
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
Example fix
// before ZipInfo filename = "/absolute/path/file" // after ZipInfo filename = "pkg/file" # relative, contained
Defensive patterns
Strategy: validation
Validate before calling
import os
from pathlib import Path
def members_stay_in_root(names, target):
root = os.path.abspath(target)
return all(os.path.abspath(os.path.join(target, n)).startswith(root + os.sep) or
os.path.abspath(os.path.join(target, n)) == root for n in names) Type guard
def member_is_contained(target: str, member: str) -> bool:
root = os.path.abspath(target)
dest = os.path.abspath(os.path.join(target, member))
return dest.startswith(root + os.sep) or dest == root Try / catch
try:
download_and_extract_archive_verified(url, target, archive_type="tar.gz")
except SolidLSPException as e:
if "path escapes extraction directory" in str(e):
raise SecurityError("archive member escapes extraction root") from e
raise Prevention
- 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
When it happens
Trigger: 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.
Common situations: Same as Zip-Slip: untrusted or poorly packaged archives downloaded during language-server setup.
Related errors
- Unsafe archive member '{member_name}': path traversal is not
- Cannot edit external file: {relative_path}
- Memory name resolves outside the memories directory. Got: {'
- Memory name cannot contain '..' segments. Got: {name}
- Memory name cannot be absolute or contain empty path segment
AI-assisted analysis of oraios/serena@7fcbca7e62 (2026-08-29).
Data as JSON: /api/errors/48f5dc5d72e71bd4.
Report an issue: GitHub.