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

  1. Use archives from trusted sources with purely relative member paths
  2. Repackage the archive so all entries are relative and contained in a single root folder
  3. 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

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


AI-assisted analysis of oraios/serena@7fcbca7e62 (2026-08-29). Data as JSON: /api/errors/48f5dc5d72e71bd4. Report an issue: GitHub.