fix(security): reject path traversal in credential file registration

This commit is contained in:
maymuneth
2026-03-30 15:06:35 +03:00
committed by Teknium
parent 0f2ea2062b
commit a97641b9f2
2 changed files with 142 additions and 4 deletions

View File

@@ -197,3 +197,110 @@ class TestIterSkillsFiles:
with patch.dict(os.environ, {"HERMES_HOME": str(hermes_home)}):
assert iter_skills_files() == []
class TestPathTraversalSecurity:
"""Path traversal and absolute path rejection.
A malicious skill could declare::
required_credential_files:
- path: '../../.ssh/id_rsa'
Without containment checks, this would mount the host's SSH private key
into the container sandbox, leaking it to the skill's execution environment.
"""
def test_dotdot_traversal_rejected(self, tmp_path, monkeypatch):
"""'../sensitive' must not escape HERMES_HOME."""
monkeypatch.setenv("HERMES_HOME", str(tmp_path / ".hermes"))
(tmp_path / ".hermes").mkdir()
# Create a sensitive file one level above hermes_home
sensitive = tmp_path / "sensitive.json"
sensitive.write_text('{"secret": "value"}')
result = register_credential_file("../sensitive.json")
assert result is False
assert get_credential_file_mounts() == []
def test_deep_traversal_rejected(self, tmp_path, monkeypatch):
"""'../../etc/passwd' style traversal must be rejected."""
hermes_home = tmp_path / ".hermes"
hermes_home.mkdir()
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
# Create a fake sensitive file outside hermes_home
ssh_dir = tmp_path / ".ssh"
ssh_dir.mkdir()
(ssh_dir / "id_rsa").write_text("PRIVATE KEY")
result = register_credential_file("../../.ssh/id_rsa")
assert result is False
assert get_credential_file_mounts() == []
def test_absolute_path_rejected(self, tmp_path, monkeypatch):
"""Absolute paths must be rejected regardless of whether they exist."""
hermes_home = tmp_path / ".hermes"
hermes_home.mkdir()
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
# Create a file at an absolute path
sensitive = tmp_path / "absolute.json"
sensitive.write_text("{}")
result = register_credential_file(str(sensitive))
assert result is False
assert get_credential_file_mounts() == []
def test_legitimate_file_still_works(self, tmp_path, monkeypatch):
"""Normal files inside HERMES_HOME must still be registered."""
hermes_home = tmp_path / ".hermes"
hermes_home.mkdir()
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
(hermes_home / "token.json").write_text('{"token": "abc"}')
result = register_credential_file("token.json")
assert result is True
mounts = get_credential_file_mounts()
assert len(mounts) == 1
assert "token.json" in mounts[0]["container_path"]
def test_nested_subdir_inside_hermes_home_allowed(self, tmp_path, monkeypatch):
"""Files in subdirectories of HERMES_HOME must be allowed."""
hermes_home = tmp_path / ".hermes"
hermes_home.mkdir()
subdir = hermes_home / "creds"
subdir.mkdir()
(subdir / "oauth.json").write_text("{}")
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
result = register_credential_file("creds/oauth.json")
assert result is True
def test_symlink_traversal_rejected(self, tmp_path, monkeypatch):
"""A symlink inside HERMES_HOME pointing outside must be rejected."""
hermes_home = tmp_path / ".hermes"
hermes_home.mkdir()
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
# Create a sensitive file outside hermes_home
sensitive = tmp_path / "sensitive.json"
sensitive.write_text('{"secret": "value"}')
# Create a symlink inside hermes_home pointing outside
symlink = hermes_home / "evil_link.json"
try:
symlink.symlink_to(sensitive)
except (OSError, NotImplementedError):
pytest.skip("Symlinks not supported on this platform")
result = register_credential_file("evil_link.json")
# The resolved path escapes HERMES_HOME — must be rejected
assert result is False
assert get_credential_file_mounts() == []