Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions test/test_datasets_utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import contextlib
import gzip
import io
import os
import pathlib
import re
Expand Down Expand Up @@ -235,6 +236,28 @@ def create_archive(root, extension, mode, content="this is the content"):
with open(file) as fh:
assert fh.read() == content

@pytest.mark.parametrize("member_name", ["../escaped.txt", "../../escaped.txt"])
def test_extract_tar_rejects_path_traversal(self, member_name, tmpdir):
# Regression test for a path-traversal vulnerability (issue #9517): a tar
# member with a "../" component must not be extracted outside to_path.
tmpdir = pathlib.Path(tmpdir)
to_path = tmpdir / "extract_here"
to_path.mkdir()
escaped_target = (to_path / member_name).resolve()
assert to_path.resolve() not in escaped_target.parents # target is really outside

archive = tmpdir / "malicious.tar"
payload = b"path traversal payload"
with tarfile.open(archive, mode="w") as tar:
info = tarfile.TarInfo(member_name)
info.size = len(payload)
tar.addfile(info, io.BytesIO(payload))

with pytest.raises((tarfile.TarError, RuntimeError)):
utils.extract_archive(str(archive), str(to_path))

assert not escaped_target.exists()

def test_verify_str_arg(self):
assert "a" == utils.verify_str_arg("a", "arg", ("a",))
pytest.raises(ValueError, utils.verify_str_arg, 0, ("a",), "arg")
Expand Down
26 changes: 25 additions & 1 deletion torchvision/datasets/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,11 +206,35 @@ def download_file_from_google_drive(
raise RuntimeError("File not found or corrupted.")


def _reject_escaping_tar_members(tar: tarfile.TarFile, to_path: Union[str, pathlib.Path]) -> None:
"""Path-traversal guard for Python versions without the PEP 706 ``data`` filter.

Rejects any member whose resolved path, or link target, would fall outside
``to_path`` before extraction writes anything.
"""
base = os.path.realpath(to_path)
for member in tar.getmembers():
target = os.path.realpath(os.path.join(base, member.name))
if os.path.commonpath([base, target]) != base:
raise RuntimeError(f"Refusing to extract tar member '{member.name}' outside of '{to_path}'.")
if member.issym() or member.islnk():
link_target = os.path.realpath(os.path.join(os.path.dirname(target), member.linkname))
if os.path.commonpath([base, link_target]) != base:
raise RuntimeError(f"Refusing to extract tar member '{member.name}' linking outside of '{to_path}'.")


def _extract_tar(
from_path: Union[str, pathlib.Path], to_path: Union[str, pathlib.Path], compression: Optional[str]
) -> None:
with tarfile.open(from_path, f"r:{compression[1:]}" if compression else "r") as tar:
tar.extractall(to_path)
# PEP 706 ``data`` filter rejects path traversal, absolute paths, and links
# escaping ``to_path``. It is the default in Python 3.14 and available since
# 3.12 (backported to 3.10.12+/3.11.4+); fall back to a manual guard otherwise.
if hasattr(tarfile, "data_filter"):
tar.extractall(to_path, filter="data")
else:
_reject_escaping_tar_members(tar, to_path)
tar.extractall(to_path)


_ZIP_COMPRESSION_MAP: dict[str, int] = {
Expand Down