mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-02 07:11:49 +00:00
fix(skills): reject symlinks in skill bundles before install
This commit is contained in:
parent
fe9744cbee
commit
c26af46811
2 changed files with 62 additions and 0 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue