kovidgoyal/kitty · critical · ValueError

Attempted path traversal in tar file: {member.name}

Error message

Attempted path traversal in tar file: {member.name}

What it means

safe_extract() (used by extract_all_from_tarfile_safely) walks every tar member and verifies that os.path.join(dest, member.name) stays inside the destination directory. A member whose absolute path or ../ components escapes the destination raises ValueError — this is an explicit defense against the classic tar path-traversal (TarSlip) attack before extractall runs.

Source

Thrown at kitty/utils.py:1137

        log_error(e)
        url = 'Unparseable URL: ' + url
    return url


def extract_all_from_tarfile_safely(tf: 'tarfile.TarFile', dest: str) -> None:
    # Ensure that all extracted items are within dest

    def is_within_directory(directory: str, target: str) -> bool:
        abs_directory = os.path.abspath(directory)
        abs_target = os.path.abspath(target)
        prefix = os.path.commonprefix((abs_directory, abs_target))
        return prefix == abs_directory

    def safe_extract(tar: 'tarfile.TarFile', path: str = '.', numeric_owner: bool = False) -> None:
        for member in tar.getmembers():
            member_path = os.path.join(path, member.name)
            if not is_within_directory(path, member_path):
                raise ValueError(f'Attempted path traversal in tar file: {member.name}')
        tar.extractall(path, tar.getmembers(), numeric_owner=numeric_owner)

    safe_extract(tf, dest)


def is_png(path: str) -> bool:
    if path:
        with suppress(Exception), open(path, 'rb') as f:
            header = f.read(8)
            return header.startswith(b'\211PNG\r\n\032\n')
    return False


def cmdline_for_hold(cmd: Sequence[str] = (), opts: Optional['Options'] = None) -> list[str]:
    if opts is None:
        with suppress(RuntimeError):
            opts = get_options()
    if opts is None:

View on GitHub (pinned to 6d5d0c4406)

Solutions

  1. Reject or quarantine the untrusted archive — the error is a security stop, do not bypass it
  2. Re-pack the archive from within the source directory so members are relative paths
  3. Sanitize members (strip leading '/' and resolve '..') before building the tar, if you control creation
  4. Never wrap the call in a blind except that falls back to tar.extractall

Example fix

# before
tar.add('/etc/config.conf')  # stores absolute path
# after
tar.add('/etc/config.conf', arcname='config.conf')  # relative member
Defensive patterns

Strategy: validation

Validate before calling

import os
for m in tar.getmembers():
    dest = os.path.realpath(os.path.join(extract_dir, m.name))
    if not dest.startswith(os.path.realpath(extract_dir) + os.sep):
        raise ValueError(f'unsafe member {m.name}')

Try / catch

try:
    extract_all_from_tarfile_safely(tf, dest)
except ValueError as e:
    if 'path traversal' in str(e):
        reject_archive(tf)  # quarantine / alert; never fall back to extractall
    else:
        raise

Prevention

When it happens

Trigger: A tar archive containing entries like '../../etc/passwd', '/etc/passwd', or symlinks whose resolved member path falls outside the extraction directory; the check runs on every member of any tar passed to extract_all_from_tarfile_safely.

Common situations: Processing untrusted/user-uploaded tarballs (kitty e.g. does this for remote resource tars); a legitimately-created archive with absolute paths baked in by an old tar version.

Related errors


AI-assisted analysis of kovidgoyal/kitty@6d5d0c4406 (2026-08-27). Data as JSON: /api/errors/b7edc4a8c111c9cb. Report an issue: GitHub.