fix(profile): reject symlinks in distributions (#25292)
This commit is contained in:
@ -432,6 +432,20 @@ def _stage_source(source: str, workdir: Path) -> Tuple[Path, str]:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _reject_distribution_symlinks(staged: Path) -> None:
|
||||||
|
"""Reject symlinks before reading or copying distribution files."""
|
||||||
|
for entry in staged.rglob("*"):
|
||||||
|
if not entry.is_symlink():
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
rel = entry.relative_to(staged)
|
||||||
|
except ValueError:
|
||||||
|
rel = entry
|
||||||
|
raise DistributionError(
|
||||||
|
f"Profile distributions cannot contain symlinks: {rel}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Install
|
# Install
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@ -484,6 +498,7 @@ def plan_install(
|
|||||||
from hermes_cli import __version__ as hermes_version
|
from hermes_cli import __version__ as hermes_version
|
||||||
|
|
||||||
staged, provenance = _stage_source(source, workdir)
|
staged, provenance = _stage_source(source, workdir)
|
||||||
|
_reject_distribution_symlinks(staged)
|
||||||
manifest = read_manifest(staged)
|
manifest = read_manifest(staged)
|
||||||
if manifest is None:
|
if manifest is None:
|
||||||
raise DistributionError(
|
raise DistributionError(
|
||||||
|
|||||||
@ -74,6 +74,13 @@ def _make_staging_dir(root: Path, name: str = "src", *, manifest: DistributionMa
|
|||||||
return staged
|
return staged
|
||||||
|
|
||||||
|
|
||||||
|
def _symlink_file_or_skip(link: Path, target: Path) -> None:
|
||||||
|
try:
|
||||||
|
link.symlink_to(target)
|
||||||
|
except OSError as exc:
|
||||||
|
pytest.skip(f"symlinks unavailable in test environment: {exc}")
|
||||||
|
|
||||||
|
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
# Manifest parsing
|
# Manifest parsing
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
@ -473,6 +480,23 @@ class TestSecurity:
|
|||||||
if (plan.target_dir / ".env").exists():
|
if (plan.target_dir / ".env").exists():
|
||||||
assert "LEAKED" not in (plan.target_dir / ".env").read_text()
|
assert "LEAKED" not in (plan.target_dir / ".env").read_text()
|
||||||
|
|
||||||
|
def test_install_rejects_symlinked_distribution_files(self, profile_env, tmp_path):
|
||||||
|
"""Distribution install must not follow symlinks to local files."""
|
||||||
|
staged = _make_staging_dir(profile_env, "src")
|
||||||
|
local_secret = tmp_path / "local-secret.txt"
|
||||||
|
local_secret.write_text("outside secret\n")
|
||||||
|
_symlink_file_or_skip(
|
||||||
|
staged / "skills" / "demo" / "leak.txt",
|
||||||
|
local_secret,
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(DistributionError, match="symlink"):
|
||||||
|
install_distribution(str(staged), name="clean")
|
||||||
|
|
||||||
|
from hermes_cli.profiles import get_profile_dir
|
||||||
|
target = get_profile_dir("clean")
|
||||||
|
assert not (target / "skills" / "demo" / "leak.txt").exists()
|
||||||
|
|
||||||
|
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
# Install-time metadata (installed_at stamp)
|
# Install-time metadata (installed_at stamp)
|
||||||
@ -581,4 +605,3 @@ class TestErrorSurfaces:
|
|||||||
staged = _make_staging_dir(profile_env, "bad", manifest=mf)
|
staged = _make_staging_dir(profile_env, "bad", manifest=mf)
|
||||||
with pytest.raises((ValueError, DistributionError)):
|
with pytest.raises((ValueError, DistributionError)):
|
||||||
plan_install(str(staged), tmp_path / "work")
|
plan_install(str(staged), tmp_path / "work")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user