193 lines
6.2 KiB
Python
193 lines
6.2 KiB
Python
"""Unit tests for archive extraction — path traversal protection, flattening, permissions."""
|
|
|
|
import io
|
|
import os
|
|
import platform
|
|
import stat
|
|
import tarfile
|
|
import zipfile
|
|
|
|
import pytest
|
|
|
|
from cloakbrowser.download import (
|
|
_extract_tar,
|
|
_extract_zip,
|
|
_flatten_single_subdir,
|
|
_is_executable,
|
|
_make_executable,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# tar.gz extraction
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _create_tar_gz(tmp_path, members: dict[str, bytes]) -> "Path":
|
|
"""Create a tar.gz with given {name: content} members."""
|
|
archive = tmp_path / "test.tar.gz"
|
|
with tarfile.open(archive, "w:gz") as tar:
|
|
for name, content in members.items():
|
|
info = tarfile.TarInfo(name=name)
|
|
info.size = len(content)
|
|
tar.addfile(info, io.BytesIO(content))
|
|
return archive
|
|
|
|
|
|
class TestExtractTar:
|
|
def test_basic(self, tmp_path):
|
|
archive = _create_tar_gz(tmp_path, {"chrome": b"binary", "lib/libfoo.so": b"lib"})
|
|
dest = tmp_path / "out"
|
|
dest.mkdir()
|
|
_extract_tar(archive, dest)
|
|
assert (dest / "chrome").read_bytes() == b"binary"
|
|
assert (dest / "lib" / "libfoo.so").read_bytes() == b"lib"
|
|
|
|
def test_path_traversal_blocked(self, tmp_path):
|
|
archive = tmp_path / "evil.tar.gz"
|
|
with tarfile.open(archive, "w:gz") as tar:
|
|
info = tarfile.TarInfo(name="../../../etc/passwd")
|
|
info.size = 4
|
|
tar.addfile(info, io.BytesIO(b"evil"))
|
|
|
|
dest = tmp_path / "out"
|
|
dest.mkdir()
|
|
with pytest.raises(RuntimeError, match="path traversal"):
|
|
_extract_tar(archive, dest)
|
|
|
|
def test_suspicious_symlink_skipped(self, tmp_path):
|
|
"""Symlinks with absolute targets are skipped (logged as warning)."""
|
|
archive = tmp_path / "symlink.tar.gz"
|
|
with tarfile.open(archive, "w:gz") as tar:
|
|
# Normal file
|
|
info = tarfile.TarInfo(name="chrome")
|
|
info.size = 6
|
|
tar.addfile(info, io.BytesIO(b"binary"))
|
|
# Suspicious symlink
|
|
sym = tarfile.TarInfo(name="evil_link")
|
|
sym.type = tarfile.SYMTYPE
|
|
sym.linkname = "/etc/passwd"
|
|
tar.addfile(sym)
|
|
|
|
dest = tmp_path / "out"
|
|
dest.mkdir()
|
|
_extract_tar(archive, dest)
|
|
# Normal file extracted
|
|
assert (dest / "chrome").exists()
|
|
# Suspicious symlink was skipped
|
|
assert not (dest / "evil_link").exists()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# zip extraction
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _create_zip(tmp_path, members: dict[str, bytes]) -> "Path":
|
|
"""Create a zip with given {name: content} members."""
|
|
archive = tmp_path / "test.zip"
|
|
with zipfile.ZipFile(archive, "w") as zf:
|
|
for name, content in members.items():
|
|
zf.writestr(name, content)
|
|
return archive
|
|
|
|
|
|
class TestExtractZip:
|
|
def test_basic(self, tmp_path):
|
|
archive = _create_zip(tmp_path, {"chrome.exe": b"binary", "lib/foo.dll": b"lib"})
|
|
dest = tmp_path / "out"
|
|
dest.mkdir()
|
|
_extract_zip(archive, dest)
|
|
assert (dest / "chrome.exe").read_bytes() == b"binary"
|
|
assert (dest / "lib" / "foo.dll").read_bytes() == b"lib"
|
|
|
|
def test_path_traversal_blocked(self, tmp_path):
|
|
archive = tmp_path / "evil.zip"
|
|
with zipfile.ZipFile(archive, "w") as zf:
|
|
zf.writestr("../../../etc/passwd", "evil")
|
|
|
|
dest = tmp_path / "out"
|
|
dest.mkdir()
|
|
with pytest.raises(RuntimeError, match="path traversal"):
|
|
_extract_zip(archive, dest)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Directory flattening
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestFlatten:
|
|
def test_single_subdir_flattened(self, tmp_path):
|
|
"""Single subdir contents moved up."""
|
|
dest = tmp_path / "out"
|
|
dest.mkdir()
|
|
subdir = dest / "fingerprint-chromium-custom-v14"
|
|
subdir.mkdir()
|
|
(subdir / "chrome").write_bytes(b"binary")
|
|
(subdir / "lib").mkdir()
|
|
|
|
_flatten_single_subdir(dest)
|
|
|
|
assert (dest / "chrome").read_bytes() == b"binary"
|
|
assert (dest / "lib").is_dir()
|
|
assert not subdir.exists()
|
|
|
|
def test_app_bundle_preserved(self, tmp_path):
|
|
""".app directory NOT flattened (macOS bundle)."""
|
|
dest = tmp_path / "out"
|
|
dest.mkdir()
|
|
app = dest / "Chromium.app"
|
|
app.mkdir()
|
|
(app / "Contents").mkdir()
|
|
(app / "Contents" / "MacOS").mkdir()
|
|
(app / "Contents" / "MacOS" / "Chromium").write_bytes(b"binary")
|
|
|
|
_flatten_single_subdir(dest)
|
|
|
|
# .app bundle kept intact
|
|
assert app.is_dir()
|
|
assert (app / "Contents" / "MacOS" / "Chromium").exists()
|
|
|
|
def test_noop_multiple_entries(self, tmp_path):
|
|
"""Multiple entries at top level — no flattening."""
|
|
dest = tmp_path / "out"
|
|
dest.mkdir()
|
|
(dest / "chrome").write_bytes(b"binary")
|
|
(dest / "lib").mkdir()
|
|
|
|
_flatten_single_subdir(dest)
|
|
|
|
# Nothing moved
|
|
assert (dest / "chrome").exists()
|
|
assert (dest / "lib").is_dir()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Permissions
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestPermissions:
|
|
@pytest.mark.skipif(platform.system() == "Windows", reason="chmod not applicable on Windows")
|
|
def test_make_executable(self, tmp_path):
|
|
binary = tmp_path / "chrome"
|
|
binary.write_bytes(b"binary")
|
|
binary.chmod(0o644)
|
|
assert not _is_executable(binary)
|
|
|
|
_make_executable(binary)
|
|
assert _is_executable(binary)
|
|
|
|
def test_is_executable_true(self, tmp_path):
|
|
binary = tmp_path / "chrome"
|
|
binary.write_bytes(b"binary")
|
|
binary.chmod(0o755)
|
|
assert _is_executable(binary)
|
|
|
|
def test_is_executable_false(self, tmp_path):
|
|
binary = tmp_path / "chrome"
|
|
binary.write_bytes(b"binary")
|
|
binary.chmod(0o644)
|
|
assert not _is_executable(binary)
|