Files
hermes-agent/tests/agent/test_skill_name_traversal.py

353 lines
13 KiB
Python
Raw Normal View History

"""Specific tests for V-011: Skills Guard Bypass via Path Traversal.
This test file focuses on the specific attack vector where malicious skill names
are used to bypass the skills security guard and access arbitrary files.
"""
import json
import pytest
from pathlib import Path
from unittest.mock import patch
class TestV011SkillsGuardBypass:
"""Tests for V-011 vulnerability fix.
V-011: Skills Guard Bypass via Path Traversal
- CVSS Score: 7.8 (High)
- Attack Vector: Local/Remote via malicious skill names
- Description: Path traversal in skill names (e.g., '../../../etc/passwd')
can bypass skill loading security controls
"""
@pytest.fixture
def setup_skills_dir(self, tmp_path):
"""Create a temporary skills directory structure."""
skills_dir = tmp_path / "skills"
skills_dir.mkdir()
# Create a legitimate skill
legit_skill = skills_dir / "legit-skill"
legit_skill.mkdir()
(legit_skill / "SKILL.md").write_text("""\
---
name: legit-skill
description: A legitimate test skill
---
# Legitimate Skill
This skill is safe.
""")
# Create sensitive files outside skills directory
hermes_dir = tmp_path / ".hermes"
hermes_dir.mkdir()
(hermes_dir / ".env").write_text("OPENAI_API_KEY=sk-test12345\nANTHROPIC_API_KEY=sk-ant-test123\n")
# Create other sensitive files
(tmp_path / "secret.txt").write_text("TOP SECRET DATA")
(tmp_path / "id_rsa").write_text("-----BEGIN OPENSSH PRIVATE KEY-----\ntest-key-data\n-----END OPENSSH PRIVATE KEY-----")
return {
"skills_dir": skills_dir,
"tmp_path": tmp_path,
"hermes_dir": hermes_dir,
}
def test_dotdot_traversal_blocked(self, setup_skills_dir):
"""Basic '../' traversal should be blocked."""
from tools.skills_tool import skill_view
skills_dir = setup_skills_dir["skills_dir"]
with patch("tools.skills_tool.SKILLS_DIR", skills_dir):
# Try to access secret.txt using traversal
result = json.loads(skill_view("../secret.txt"))
assert result["success"] is False
assert "traversal" in result.get("error", "").lower() or "security_error" in result
def test_deep_traversal_blocked(self, setup_skills_dir):
"""Deep traversal '../../../' should be blocked."""
from tools.skills_tool import skill_view
skills_dir = setup_skills_dir["skills_dir"]
with patch("tools.skills_tool.SKILLS_DIR", skills_dir):
# Try deep traversal to reach tmp_path parent
result = json.loads(skill_view("../../../secret.txt"))
assert result["success"] is False
def test_traversal_with_category_blocked(self, setup_skills_dir):
"""Traversal within category path should be blocked."""
from tools.skills_tool import skill_view
skills_dir = setup_skills_dir["skills_dir"]
# Create category structure
category_dir = skills_dir / "mlops"
category_dir.mkdir()
skill_dir = category_dir / "test-skill"
skill_dir.mkdir()
(skill_dir / "SKILL.md").write_text("# Test Skill")
with patch("tools.skills_tool.SKILLS_DIR", skills_dir):
# Try traversal from within category
result = json.loads(skill_view("mlops/../../secret.txt"))
assert result["success"] is False
def test_home_directory_expansion_blocked(self, setup_skills_dir):
"""Home directory expansion '~/' should be blocked."""
from tools.skills_tool import skill_view
from agent.skill_commands import _load_skill_payload
skills_dir = setup_skills_dir["skills_dir"]
with patch("tools.skills_tool.SKILLS_DIR", skills_dir):
# Test skill_view
result = json.loads(skill_view("~/.hermes/.env"))
assert result["success"] is False
# Test _load_skill_payload
payload = _load_skill_payload("~/.hermes/.env")
assert payload is None
def test_absolute_path_blocked(self, setup_skills_dir):
"""Absolute paths should be blocked."""
from tools.skills_tool import skill_view
from agent.skill_commands import _load_skill_payload
skills_dir = setup_skills_dir["skills_dir"]
with patch("tools.skills_tool.SKILLS_DIR", skills_dir):
# Test various absolute paths
for path in ["/etc/passwd", "/root/.ssh/id_rsa", "/.env", "/proc/self/environ"]:
result = json.loads(skill_view(path))
assert result["success"] is False, f"Absolute path {path} should be blocked"
# Test via _load_skill_payload
payload = _load_skill_payload("/etc/passwd")
assert payload is None
def test_file_protocol_blocked(self, setup_skills_dir):
"""File protocol URLs should be blocked."""
from tools.skills_tool import skill_view
skills_dir = setup_skills_dir["skills_dir"]
with patch("tools.skills_tool.SKILLS_DIR", skills_dir):
result = json.loads(skill_view("file:///etc/passwd"))
assert result["success"] is False
def test_url_encoding_traversal_blocked(self, setup_skills_dir):
"""URL-encoded traversal attempts should be blocked."""
from tools.skills_tool import skill_view
skills_dir = setup_skills_dir["skills_dir"]
with patch("tools.skills_tool.SKILLS_DIR", skills_dir):
# URL-encoded '../'
result = json.loads(skill_view("%2e%2e%2fsecret.txt"))
# This might fail validation due to % character or resolve to a non-existent skill
assert result["success"] is False or "not found" in result.get("error", "").lower()
def test_null_byte_injection_blocked(self, setup_skills_dir):
"""Null byte injection attempts should be blocked."""
from tools.skills_tool import skill_view
from agent.skill_commands import _load_skill_payload
skills_dir = setup_skills_dir["skills_dir"]
with patch("tools.skills_tool.SKILLS_DIR", skills_dir):
# Null byte injection to bypass extension checks
result = json.loads(skill_view("skill.md\x00.py"))
assert result["success"] is False
payload = _load_skill_payload("skill.md\x00.py")
assert payload is None
def test_double_traversal_blocked(self, setup_skills_dir):
"""Double traversal '....//' should be blocked."""
from tools.skills_tool import skill_view
skills_dir = setup_skills_dir["skills_dir"]
with patch("tools.skills_tool.SKILLS_DIR", skills_dir):
# Double dot encoding
result = json.loads(skill_view("....//secret.txt"))
assert result["success"] is False
def test_traversal_with_null_in_middle_blocked(self, setup_skills_dir):
"""Traversal with embedded null bytes should be blocked."""
from tools.skills_tool import skill_view
skills_dir = setup_skills_dir["skills_dir"]
with patch("tools.skills_tool.SKILLS_DIR", skills_dir):
result = json.loads(skill_view("../\x00/../secret.txt"))
assert result["success"] is False
def test_windows_path_traversal_blocked(self, setup_skills_dir):
"""Windows-style path traversal should be blocked."""
from tools.skills_tool import skill_view
skills_dir = setup_skills_dir["skills_dir"]
with patch("tools.skills_tool.SKILLS_DIR", skills_dir):
# Windows-style paths
for path in ["..\\secret.txt", "..\\..\\secret.txt", "C:\\secret.txt"]:
result = json.loads(skill_view(path))
assert result["success"] is False, f"Windows path {path} should be blocked"
def test_mixed_separator_traversal_blocked(self, setup_skills_dir):
"""Mixed separator traversal should be blocked."""
from tools.skills_tool import skill_view
skills_dir = setup_skills_dir["skills_dir"]
with patch("tools.skills_tool.SKILLS_DIR", skills_dir):
# Mixed forward and back slashes
result = json.loads(skill_view("../\\../secret.txt"))
assert result["success"] is False
def test_legitimate_skill_with_hyphens_works(self, setup_skills_dir):
"""Legitimate skill names with hyphens should work."""
from tools.skills_tool import skill_view
from agent.skill_commands import _load_skill_payload
skills_dir = setup_skills_dir["skills_dir"]
with patch("tools.skills_tool.SKILLS_DIR", skills_dir):
# Test legitimate skill
result = json.loads(skill_view("legit-skill"))
assert result["success"] is True
assert result.get("name") == "legit-skill"
# Test via _load_skill_payload
payload = _load_skill_payload("legit-skill")
assert payload is not None
def test_legitimate_skill_with_underscores_works(self, setup_skills_dir):
"""Legitimate skill names with underscores should work."""
from tools.skills_tool import skill_view
skills_dir = setup_skills_dir["skills_dir"]
# Create skill with underscore
skill_dir = skills_dir / "my_skill"
skill_dir.mkdir()
(skill_dir / "SKILL.md").write_text("""\
---
name: my_skill
description: Test skill
---
# My Skill
""")
with patch("tools.skills_tool.SKILLS_DIR", skills_dir):
result = json.loads(skill_view("my_skill"))
assert result["success"] is True
def test_legitimate_category_skill_works(self, setup_skills_dir):
"""Legitimate category/skill paths should work."""
from tools.skills_tool import skill_view
skills_dir = setup_skills_dir["skills_dir"]
# Create category structure
category_dir = skills_dir / "mlops"
category_dir.mkdir()
skill_dir = category_dir / "axolotl"
skill_dir.mkdir()
(skill_dir / "SKILL.md").write_text("""\
---
name: axolotl
description: ML training skill
---
# Axolotl
""")
with patch("tools.skills_tool.SKILLS_DIR", skills_dir):
result = json.loads(skill_view("mlops/axolotl"))
assert result["success"] is True
assert result.get("name") == "axolotl"
class TestSkillViewFilePathSecurity:
"""Tests for file_path parameter security in skill_view."""
@pytest.fixture
def setup_skill_with_files(self, tmp_path):
"""Create a skill with supporting files."""
skills_dir = tmp_path / "skills"
skills_dir.mkdir()
skill_dir = skills_dir / "test-skill"
skill_dir.mkdir()
(skill_dir / "SKILL.md").write_text("# Test Skill")
# Create references directory
refs = skill_dir / "references"
refs.mkdir()
(refs / "api.md").write_text("# API Documentation")
# Create secret file outside skill
(tmp_path / "secret.txt").write_text("SECRET")
return {"skills_dir": skills_dir, "skill_dir": skill_dir, "tmp_path": tmp_path}
def test_file_path_traversal_blocked(self, setup_skill_with_files):
"""Path traversal in file_path parameter should be blocked."""
from tools.skills_tool import skill_view
skills_dir = setup_skill_with_files["skills_dir"]
with patch("tools.skills_tool.SKILLS_DIR", skills_dir):
result = json.loads(skill_view("test-skill", file_path="../../secret.txt"))
assert result["success"] is False
assert "traversal" in result.get("error", "").lower()
def test_file_path_absolute_blocked(self, setup_skill_with_files):
"""Absolute paths in file_path should be handled safely."""
from tools.skills_tool import skill_view
skills_dir = setup_skill_with_files["skills_dir"]
with patch("tools.skills_tool.SKILLS_DIR", skills_dir):
# Absolute paths should be rejected
result = json.loads(skill_view("test-skill", file_path="/etc/passwd"))
assert result["success"] is False
def test_legitimate_file_path_works(self, setup_skill_with_files):
"""Legitimate file paths within skill should work."""
from tools.skills_tool import skill_view
skills_dir = setup_skill_with_files["skills_dir"]
with patch("tools.skills_tool.SKILLS_DIR", skills_dir):
result = json.loads(skill_view("test-skill", file_path="references/api.md"))
assert result["success"] is True
assert "API Documentation" in result.get("content", "")
class TestSecurityLogging:
"""Tests for security event logging."""
def test_traversal_attempt_logged(self, tmp_path, caplog):
"""Path traversal attempts should be logged as warnings."""
import logging
from tools.skills_tool import skill_view
skills_dir = tmp_path / "skills"
skills_dir.mkdir()
with patch("tools.skills_tool.SKILLS_DIR", skills_dir):
with caplog.at_level(logging.WARNING):
result = json.loads(skill_view("../../../etc/passwd"))
assert result["success"] is False
# Check that a warning was logged
assert any("security" in record.message.lower() or "traversal" in record.message.lower()
for record in caplog.records)