162 lines
5.0 KiB
Python
162 lines
5.0 KiB
Python
|
|
"""Comprehensive tests for path traversal protection (V-002).
|
||
|
|
|
||
|
|
Validates that file operations correctly block malicious paths.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
import os
|
||
|
|
import tempfile
|
||
|
|
from pathlib import Path
|
||
|
|
from unittest.mock import MagicMock, patch
|
||
|
|
|
||
|
|
from tools.file_operations import (
|
||
|
|
_contains_path_traversal,
|
||
|
|
_validate_safe_path,
|
||
|
|
ShellFileOperations,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
class TestPathTraversalDetection:
|
||
|
|
"""Test path traversal pattern detection."""
|
||
|
|
|
||
|
|
@pytest.mark.parametrize("path,expected", [
|
||
|
|
# Unix-style traversal
|
||
|
|
("../../../etc/passwd", True),
|
||
|
|
("../secret.txt", True),
|
||
|
|
("foo/../../bar", True),
|
||
|
|
|
||
|
|
# Windows-style traversal
|
||
|
|
("..\\..\\windows\\system32", True),
|
||
|
|
("foo\\..\\bar", True),
|
||
|
|
|
||
|
|
# URL-encoded
|
||
|
|
("%2e%2e%2fetc%2fpasswd", True),
|
||
|
|
("%2E%2E/%2Ftest", True),
|
||
|
|
|
||
|
|
# Double slash
|
||
|
|
("..//..//etc/passwd", True),
|
||
|
|
|
||
|
|
# Tilde escape
|
||
|
|
("~/../../../etc/shadow", True),
|
||
|
|
|
||
|
|
# Null byte injection
|
||
|
|
("/etc/passwd\x00.txt", True),
|
||
|
|
|
||
|
|
# Safe paths
|
||
|
|
("/home/user/file.txt", False),
|
||
|
|
("./relative/path", False),
|
||
|
|
("~/documents/file", False),
|
||
|
|
("normal_file_name", False),
|
||
|
|
])
|
||
|
|
def test_contains_path_traversal(self, path, expected):
|
||
|
|
"""Test traversal pattern detection."""
|
||
|
|
result = _contains_path_traversal(path)
|
||
|
|
assert result == expected, f"Path: {repr(path)}"
|
||
|
|
|
||
|
|
|
||
|
|
class TestPathValidation:
|
||
|
|
"""Test comprehensive path validation."""
|
||
|
|
|
||
|
|
def test_validate_safe_path_valid(self):
|
||
|
|
"""Test valid paths pass validation."""
|
||
|
|
valid_paths = [
|
||
|
|
"/home/user/file.txt",
|
||
|
|
"./relative/path",
|
||
|
|
"~/documents",
|
||
|
|
"normal_file",
|
||
|
|
]
|
||
|
|
for path in valid_paths:
|
||
|
|
is_safe, error = _validate_safe_path(path)
|
||
|
|
assert is_safe is True, f"Path should be valid: {path} - {error}"
|
||
|
|
|
||
|
|
def test_validate_safe_path_traversal(self):
|
||
|
|
"""Test traversal paths are rejected."""
|
||
|
|
is_safe, error = _validate_safe_path("../../../etc/passwd")
|
||
|
|
assert is_safe is False
|
||
|
|
assert "Path traversal" in error
|
||
|
|
|
||
|
|
def test_validate_safe_path_null_byte(self):
|
||
|
|
"""Test null byte injection is blocked."""
|
||
|
|
is_safe, error = _validate_safe_path("/etc/passwd\x00.txt")
|
||
|
|
assert is_safe is False
|
||
|
|
|
||
|
|
def test_validate_safe_path_empty(self):
|
||
|
|
"""Test empty path is rejected."""
|
||
|
|
is_safe, error = _validate_safe_path("")
|
||
|
|
assert is_safe is False
|
||
|
|
assert "empty" in error.lower()
|
||
|
|
|
||
|
|
def test_validate_safe_path_control_chars(self):
|
||
|
|
"""Test control characters are blocked."""
|
||
|
|
is_safe, error = _validate_safe_path("/path/with/\x01/control")
|
||
|
|
assert is_safe is False
|
||
|
|
assert "control" in error.lower()
|
||
|
|
|
||
|
|
def test_validate_safe_path_very_long(self):
|
||
|
|
"""Test overly long paths are rejected."""
|
||
|
|
long_path = "a" * 5000
|
||
|
|
is_safe, error = _validate_safe_path(long_path)
|
||
|
|
assert is_safe is False
|
||
|
|
|
||
|
|
|
||
|
|
class TestShellFileOperationsSecurity:
|
||
|
|
"""Test security integration in ShellFileOperations."""
|
||
|
|
|
||
|
|
def test_read_file_blocks_traversal(self):
|
||
|
|
"""Test read_file rejects traversal paths."""
|
||
|
|
mock_env = MagicMock()
|
||
|
|
ops = ShellFileOperations(mock_env)
|
||
|
|
|
||
|
|
result = ops.read_file("../../../etc/passwd")
|
||
|
|
assert result.error is not None
|
||
|
|
assert "Security violation" in result.error
|
||
|
|
|
||
|
|
def test_write_file_blocks_traversal(self):
|
||
|
|
"""Test write_file rejects traversal paths."""
|
||
|
|
mock_env = MagicMock()
|
||
|
|
ops = ShellFileOperations(mock_env)
|
||
|
|
|
||
|
|
result = ops.write_file("../../../etc/cron.d/backdoor", "malicious")
|
||
|
|
assert result.error is not None
|
||
|
|
assert "Security violation" in result.error
|
||
|
|
|
||
|
|
|
||
|
|
class TestEdgeCases:
|
||
|
|
"""Test edge cases and bypass attempts."""
|
||
|
|
|
||
|
|
@pytest.mark.parametrize("path", [
|
||
|
|
# Mixed case
|
||
|
|
"..%2F..%2Fetc%2Fpasswd",
|
||
|
|
"%2e.%2f",
|
||
|
|
# Unicode normalization bypasses
|
||
|
|
"\u2025\u2025/etc/passwd", # Double dot characters
|
||
|
|
"\u2024\u2024/etc/passwd", # One dot characters
|
||
|
|
])
|
||
|
|
def test_advanced_bypass_attempts(self, path):
|
||
|
|
"""Test advanced bypass attempts."""
|
||
|
|
# These should be caught by length or control char checks
|
||
|
|
is_safe, _ = _validate_safe_path(path)
|
||
|
|
# At minimum, shouldn't crash
|
||
|
|
assert isinstance(is_safe, bool)
|
||
|
|
|
||
|
|
|
||
|
|
class TestPerformance:
|
||
|
|
"""Test validation performance with many paths."""
|
||
|
|
|
||
|
|
def test_bulk_validation_performance(self):
|
||
|
|
"""Test that bulk validation is fast."""
|
||
|
|
import time
|
||
|
|
|
||
|
|
paths = [
|
||
|
|
"/home/user/file" + str(i) + ".txt"
|
||
|
|
for i in range(1000)
|
||
|
|
]
|
||
|
|
|
||
|
|
start = time.time()
|
||
|
|
for path in paths:
|
||
|
|
_validate_safe_path(path)
|
||
|
|
elapsed = time.time() - start
|
||
|
|
|
||
|
|
# Should complete 1000 validations in under 1 second
|
||
|
|
assert elapsed < 1.0, f"Validation too slow: {elapsed}s"
|