Batu Lab NotesPractical developer guides

Limit an in-memory tar member name to a safe relative path

By Batu · English technical notes

Also published in our Blogger archive.

Tar member names use slash-separated archive paths, so this example treats the candidate as a PurePosixPath rather than resolving it against a real filesystem. safe_member_name rejects absolute paths, parent-directory components, backslashes, NUL bytes, and Windows drive-qualified spellings such as C:/escape.txt. For the accepted input, notes/fixture.txt, it returns a normalized POSIX spelling suitable for the member name.

The code then creates an archive in io.BytesIO, constructs a TarInfo object, and supplies another BytesIO object as the member’s content. Reopening the archive and using extractfile checks that the member name and bytes survived archive serialization. It never calls extract or extractall, so it does not write the member to disk. The rejected-input loop makes the policy concrete for traversal, absolute, backslash-containing, and drive-qualified examples.

This is a narrow name-validation rule, not a complete archive-security policy. It does not inspect symlink targets, member types, duplicate names, resource limits, or a later modification of the archive. Python’s tar documentation warns that archive extraction has dangerous features and recommends inspection and appropriate extraction filters for untrusted archives. pathlib was added in Python 3.4, making Python 3.4 the minimum for this example; tarfile itself is older. Read the tarfile security guidance, TarInfo reference, and pathlib documentation.

import io
import tarfile
from pathlib import PurePosixPath, PureWindowsPath


def safe_member_name(name):
    if "\x00" in name or "\\" in name:
        raise ValueError("name contains a disallowed character")
    posix_path = PurePosixPath(name)
    windows_path = PureWindowsPath(name)
    if (
        posix_path.is_absolute()
        or not posix_path.parts
        or ".." in posix_path.parts
        or windows_path.is_absolute()
        or windows_path.drive
    ):
        raise ValueError("name is not a safe relative path")
    return posix_path.as_posix()


payload = b"tar fixture\n"
member_name = safe_member_name("notes/fixture.txt")

for unsafe in ("../escape.txt", "/absolute.txt", "notes\\escape.txt", "C:/escape.txt"):
    try:
        safe_member_name(unsafe)
    except ValueError:
        pass
    else:
        raise AssertionError(unsafe)

archive = io.BytesIO()
with tarfile.open(fileobj=archive, mode="w") as tar:
    info = tarfile.TarInfo(member_name)
    info.size = len(payload)
    tar.addfile(info, io.BytesIO(payload))

archive.seek(0)
with tarfile.open(fileobj=archive, mode="r") as tar:
    member = tar.getmember(member_name)
    extracted = tar.extractfile(member)
    assert extracted is not None
    assert extracted.read() == payload
    assert member.name == member_name

print("member=" + member_name)
member=notes/fixture.txt

AI assistance disclosure: this article was drafted with AI assistance and should be reviewed in the context of its project.