Be strict about XDG_CACHE_DIR ownership etc

This commit is contained in:
Miguel Jacq 2026-06-22 17:22:27 +10:00
parent 4277e029d0
commit efb6d7cc15
Signed by: mig5
GPG key ID: 03906B4110AAD3B8
2 changed files with 53 additions and 9 deletions

View file

@ -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:

View file

@ -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)