"""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)