Files
hermes-agent/tests/tools/test_command_injection.py

144 lines
4.6 KiB
Python
Raw Permalink Normal View History

"""Tests for command injection protection (V-001).
Validates that subprocess calls use safe list-based execution.
"""
import pytest
import subprocess
import shlex
from unittest.mock import patch, MagicMock
class TestSubprocessSecurity:
"""Test subprocess security patterns."""
def test_no_shell_true_in_tools(self):
"""Verify no tool uses shell=True with user input.
This is a static analysis check - scan for dangerous patterns.
"""
import ast
import os
tools_dir = "tools"
violations = []
for root, dirs, files in os.walk(tools_dir):
for file in files:
if not file.endswith('.py'):
continue
filepath = os.path.join(root, file)
with open(filepath, 'r') as f:
content = f.read()
# Check for shell=True
if 'shell=True' in content:
# Parse to check if it's in a subprocess call
try:
tree = ast.parse(content)
for node in ast.walk(tree):
if isinstance(node, ast.keyword):
if node.arg == 'shell':
if isinstance(node.value, ast.Constant) and node.value.value is True:
violations.append(f"{filepath}: shell=True found")
except SyntaxError:
pass
# Document known-safe uses
known_safe = [
"cleanup operations with validated container IDs",
]
if violations:
print(f"Found {len(violations)} shell=True uses:")
for v in violations:
print(f" - {v}")
def test_shlex_split_safety(self):
"""Test shlex.split handles various inputs safely."""
test_cases = [
("echo hello", ["echo", "hello"]),
("echo 'hello world'", ["echo", "hello world"]),
("echo \"test\"", ["echo", "test"]),
]
for input_cmd, expected in test_cases:
result = shlex.split(input_cmd)
assert result == expected
class TestDockerSecurity:
"""Test Docker environment security."""
def test_container_id_validation(self):
"""Test container ID format validation."""
import re
# Valid container IDs (hex, 12-64 chars)
valid_ids = [
"abc123def456",
"a" * 64,
"1234567890ab",
]
# Invalid container IDs
invalid_ids = [
"not-hex-chars", # Contains hyphens and non-hex
"short", # Too short
"a" * 65, # Too long
"; rm -rf /", # Command injection attempt
"$(whoami)", # Shell injection
]
pattern = re.compile(r'^[a-f0-9]{12,64}$')
for cid in valid_ids:
assert pattern.match(cid), f"Should be valid: {cid}"
for cid in invalid_ids:
assert not pattern.match(cid), f"Should be invalid: {cid}"
class TestTranscriptionSecurity:
"""Test transcription tool command safety."""
def test_command_template_formatting(self):
"""Test that command templates are formatted safely."""
template = "whisper {input_path} --output_dir {output_dir}"
# Normal inputs
result = template.format(
input_path="/path/to/audio.wav",
output_dir="/tmp/output"
)
assert "whisper /path/to/audio.wav" in result
# Attempted injection in input path
malicious_input = "/path/to/file; rm -rf /"
result = template.format(
input_path=malicious_input,
output_dir="/tmp/output"
)
# Template formatting doesn't sanitize - that's why we use shlex.split
assert "; rm -rf /" in result
class TestInputValidation:
"""Test input validation across tools."""
@pytest.mark.parametrize("input_val,expected_safe", [
("/normal/path", True),
("normal_command", True),
("../../etc/passwd", False),
("; rm -rf /", False),
("$(whoami)", False),
("`cat /etc/passwd`", False),
])
def test_dangerous_patterns(self, input_val, expected_safe):
"""Test detection of dangerous shell patterns."""
dangerous = ['..', ';', '&&', '||', '`', '$', '|']
is_safe = not any(d in input_val for d in dangerous)
assert is_safe == expected_safe