Be strict about XDG_CACHE_DIR ownership etc
This commit is contained in:
parent
4277e029d0
commit
efb6d7cc15
2 changed files with 53 additions and 9 deletions
|
|
@ -8,6 +8,8 @@ from datetime import datetime
|
|||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from .harvest_safety import OutputSafetyError, ensure_private_dir
|
||||
|
||||
|
||||
def _safe_component(s: str) -> str:
|
||||
s = s.strip()
|
||||
|
|
@ -44,16 +46,17 @@ class HarvestCache:
|
|||
|
||||
|
||||
def _ensure_dir_secure(path: Path) -> None:
|
||||
"""Create a directory with restrictive permissions; refuse symlinks."""
|
||||
# Refuse a symlink at the leaf.
|
||||
if path.exists() and path.is_symlink():
|
||||
raise RuntimeError(f"Refusing to use symlink path: {path}")
|
||||
path.mkdir(parents=True, exist_ok=True, mode=0o700)
|
||||
"""Create a private cache directory with output-path safety checks.
|
||||
|
||||
Cache roots are persistent, so existing directories are allowed, but they
|
||||
still need the same symlink-component and root-parent trust checks as
|
||||
plaintext harvest/manifest output paths.
|
||||
"""
|
||||
|
||||
try:
|
||||
os.chmod(path, 0o700)
|
||||
except OSError:
|
||||
# Best-effort; on some FS types chmod may fail.
|
||||
pass
|
||||
ensure_private_dir(path, label="cache directory")
|
||||
except OutputSafetyError as e:
|
||||
raise RuntimeError(str(e)) from e
|
||||
|
||||
|
||||
def new_harvest_cache_dir(*, hint: Optional[str] = None) -> HarvestCache:
|
||||
|
|
|
|||
|
|
@ -95,3 +95,44 @@ def test_enroll_cache_dir_uses_default_when_xdg_not_set(monkeypatch):
|
|||
monkeypatch.delenv("XDG_CACHE_HOME", raising=False)
|
||||
result = enroll_cache_dir()
|
||||
assert str(result).endswith("/.local/cache/enroll")
|
||||
|
||||
|
||||
def test_ensure_dir_secure_refuses_symlink_parent(tmp_path: Path):
|
||||
from enroll.cache import _ensure_dir_secure
|
||||
|
||||
target = tmp_path / "target"
|
||||
target.mkdir()
|
||||
link = tmp_path / "link"
|
||||
link.symlink_to(target, target_is_directory=True)
|
||||
|
||||
with pytest.raises(RuntimeError, match="symlink"):
|
||||
_ensure_dir_secure(link / "enroll" / "harvest")
|
||||
|
||||
assert not (target / "enroll" / "harvest").exists()
|
||||
|
||||
|
||||
def test_ensure_dir_secure_rejects_unsafe_root_parent(tmp_path: Path, monkeypatch):
|
||||
from enroll.cache import _ensure_dir_secure
|
||||
import enroll.harvest_safety as hs
|
||||
|
||||
untrusted = tmp_path / "untrusted"
|
||||
untrusted.mkdir()
|
||||
untrusted.chmod(0o777)
|
||||
|
||||
monkeypatch.setattr(hs, "_effective_uid", lambda: 0)
|
||||
with pytest.raises(RuntimeError, match="not owned by root|writable by group/other"):
|
||||
_ensure_dir_secure(untrusted / "cache")
|
||||
|
||||
|
||||
def test_ensure_dir_secure_rejects_existing_file_when_not_root(
|
||||
tmp_path: Path, monkeypatch
|
||||
):
|
||||
from enroll.cache import _ensure_dir_secure
|
||||
import enroll.harvest_safety as hs
|
||||
|
||||
path = tmp_path / "cache"
|
||||
path.write_text("not a dir", encoding="utf-8")
|
||||
|
||||
monkeypatch.setattr(hs, "_effective_uid", lambda: 1000)
|
||||
with pytest.raises(RuntimeError, match="not a directory"):
|
||||
_ensure_dir_secure(path)
|
||||
|
|
|
|||
Reference in a new issue