fix(skills): reject symlinks in skill bundles before install

This commit is contained in:
MorAlekss 2026-05-25 20:51:55 -07:00 committed by Teknium
parent fe9744cbee
commit c26af46811
2 changed files with 62 additions and 0 deletions

View file

@ -1913,3 +1913,50 @@ class TestInstallPathSafety:
assert ok is False
assert victim.exists()
assert (victim / "important").read_text() == "don't delete me"
def test_install_from_quarantine_rejects_symlinks(self, tmp_path):
"""Skill install must not follow symlinks that leak file contents
from outside the quarantine directory."""
import tools.skills_hub as hub
from tools.skills_guard import ScanResult
skills_dir = tmp_path / "skills"
quarantine_root = skills_dir / ".hub" / "quarantine"
quarantine_root.mkdir(parents=True)
q_dir = quarantine_root / "pending"
q_dir.mkdir()
(q_dir / "SKILL.md").write_text("---\nname: bad-skill\n---\n")
secret = tmp_path / "secret.txt"
secret.write_text("data exfiltration payload\n")
leak = q_dir / "leak.txt"
try:
leak.symlink_to(secret)
except (OSError, NotImplementedError):
pytest.skip("symlink creation unsupported on this platform")
bundle = hub.SkillBundle(
name="bad-skill",
files={"SKILL.md": "---\nname: bad-skill\n---\n"},
source="community",
identifier="x",
trust_level="community",
)
scan_result = ScanResult(
skill_name="bad-skill",
source="community",
trust_level="community",
verdict="safe",
)
with patch.object(hub, "SKILLS_DIR", skills_dir), \
patch.object(hub, "QUARANTINE_DIR", quarantine_root):
with pytest.raises(ValueError, match="symlink"):
hub.install_from_quarantine(
q_dir, "bad-skill", "", bundle, scan_result,
)
assert not (skills_dir / "bad-skill" / "leak.txt").exists()
assert secret.read_text() == "data exfiltration payload\n"

View file

@ -3040,6 +3040,21 @@ def install_from_quarantine(
except OSError:
pass
# Reject symlinks inside the quarantined skill before moving it.
# A malicious skill bundle could include a symlink pointing outside the
# skills tree; its target contents would then be copied into skills/ and
# leaked to the agent on the next skill_view call.
for entry in quarantine_path.rglob("*"):
if not _is_path_redirect(entry):
continue
try:
rel = entry.relative_to(quarantine_resolved)
except ValueError:
rel = entry
raise ValueError(
f"Installed skill contains symlinks, which is not allowed: {rel}"
)
install_dir.parent.mkdir(parents=True, exist_ok=True)
shutil.move(str(quarantine_path), str(install_dir))