odysseus / tests /test_backup_cli_security.py
Bilal50's picture
Deploy Odysseus to Hugging Face Spaces
583170f verified
Raw
History Blame Contribute Delete
3.84 kB
import io
import tarfile
from pathlib import Path
from types import SimpleNamespace
import pytest
from tests.helpers.cli_loader import load_script
def _load_backup_cli():
return load_script("odysseus-backup")
def _patch_repo(module, monkeypatch, root: Path):
monkeypatch.setattr(module, "_REPO_ROOT", root)
monkeypatch.setattr(module, "_DATA_DIR", root / "data")
def _restore_args(path: Path):
return SimpleNamespace(path=str(path), yes=True, pretty=False)
def _verify_args(path: Path):
return SimpleNamespace(path=str(path), pretty=False)
def test_snapshot_rejects_output_inside_data_dir(tmp_path, monkeypatch):
backup = _load_backup_cli()
repo = tmp_path / "repo"
data = repo / "data"
data.mkdir(parents=True)
_patch_repo(backup, monkeypatch, repo)
with pytest.raises(SystemExit):
backup._reject_output_inside_data(data / "self.tar.gz")
def test_restore_rejects_symlink_escape(tmp_path, monkeypatch):
backup = _load_backup_cli()
repo = tmp_path / "repo"
data = repo / "data"
outside = tmp_path / "outside"
data.mkdir(parents=True)
outside.mkdir()
(data / "keep.txt").write_text("still here", encoding="utf-8")
_patch_repo(backup, monkeypatch, repo)
tar_path = tmp_path / "malicious.tar.gz"
with tarfile.open(tar_path, "w:gz") as tar:
data_dir = tarfile.TarInfo("data")
data_dir.type = tarfile.DIRTYPE
tar.addfile(data_dir)
link = tarfile.TarInfo("data/link")
link.type = tarfile.SYMTYPE
link.linkname = str(outside)
tar.addfile(link)
payload = b"escaped"
escaped = tarfile.TarInfo("data/link/pwned.txt")
escaped.size = len(payload)
tar.addfile(escaped, io.BytesIO(payload))
with pytest.raises(SystemExit):
backup.cmd_restore(_restore_args(tar_path))
assert not (outside / "pwned.txt").exists()
assert (data / "keep.txt").read_text(encoding="utf-8") == "still here"
def test_verify_rejects_symlink_escape(tmp_path):
backup = _load_backup_cli()
tar_path = tmp_path / "malicious.tar.gz"
with tarfile.open(tar_path, "w:gz") as tar:
link = tarfile.TarInfo("data/link")
link.type = tarfile.SYMTYPE
link.linkname = "/tmp"
tar.addfile(link)
with pytest.raises(SystemExit):
backup.cmd_verify(_verify_args(tar_path))
def test_restore_rejects_hardlink_entries(tmp_path, monkeypatch):
backup = _load_backup_cli()
repo = tmp_path / "repo"
(repo / "data").mkdir(parents=True)
_patch_repo(backup, monkeypatch, repo)
tar_path = tmp_path / "hardlink.tar.gz"
with tarfile.open(tar_path, "w:gz") as tar:
link = tarfile.TarInfo("data/hardlink")
link.type = tarfile.LNKTYPE
link.linkname = "../outside.txt"
tar.addfile(link)
with pytest.raises(SystemExit):
backup.cmd_restore(_restore_args(tar_path))
def test_restore_extracts_regular_files_without_extractall(tmp_path, monkeypatch):
backup = _load_backup_cli()
repo = tmp_path / "repo"
data = repo / "data"
data.mkdir(parents=True)
(data / "old.txt").write_text("old", encoding="utf-8")
_patch_repo(backup, monkeypatch, repo)
tar_path = tmp_path / "valid.tar.gz"
with tarfile.open(tar_path, "w:gz") as tar:
folder = tarfile.TarInfo("data/nested")
folder.type = tarfile.DIRTYPE
tar.addfile(folder)
payload = b"new"
item = tarfile.TarInfo("data/nested/new.txt")
item.size = len(payload)
tar.addfile(item, io.BytesIO(payload))
backup.cmd_restore(_restore_args(tar_path))
assert (repo / "data" / "nested" / "new.txt").read_text(encoding="utf-8") == "new"
assert not (repo / "data" / "old.txt").exists()
assert list(repo.glob("data.before-restore-*"))