Limit an in-memory tar member name to a safe relative path
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.
Comments
Post a Comment