"""
Input Validation Security Tests
Tests for parameter validation, injection prevention,
and input sanitization.
Test Coverage:
- TC-5.1: Parameter Validation
- TC-5.2: Injection Testing
"""
import pytest
from typing import Optional
from unittest.mock import Mock, patch
from canvas_mcp.core.validation import validate_parameter
class TestParameterValidation:
"""Test input parameter validation."""
def test_invalid_parameter_types(self):
"""TC-5.1.1: Test invalid parameter types."""
# Test with wrong types - string can't convert to int
with pytest.raises((TypeError, ValueError)):
validate_parameter("course_id", "not_a_number", int)
def test_missing_required_parameters(self):
"""TC-5.1.1: Test missing required parameters."""
# Test that None with non-optional type raises error
with pytest.raises((TypeError, ValueError)):
validate_parameter("required_param", None, str)
def test_boundary_conditions(self):
"""TC-5.1.2: Test boundary conditions."""
# Test extremely large IDs
large_id = 2**31 # Max 32-bit integer
result = validate_parameter("id", str(large_id), int)
assert isinstance(result, int)
assert result == large_id
# Test negative numbers
negative_id = -1
result = validate_parameter("id", str(negative_id), int)
assert result == -1
def test_special_characters(self):
"""TC-5.1.2: Test special characters in strings."""
# Test with various special characters
special_chars = "'; DROP TABLE students; --"
result = validate_parameter("text", special_chars, str)
# Verify special characters handled safely
assert isinstance(result, str)
assert result == special_chars # Should pass through unchanged
def test_empty_strings(self):
"""Test empty string handling."""
# Test empty string - should be valid for str type
result = validate_parameter("optional", "", str)
assert result == ""
# Empty string to int should fail
with pytest.raises(ValueError):
validate_parameter("required", "", int)
def test_none_values(self):
"""Test None value handling."""
# None for required (non-optional) parameter
with pytest.raises((TypeError, ValueError)):
validate_parameter("required", None, str)
# None for Optional parameter (should be OK)
result = validate_parameter("optional", None, Optional[str])
assert result is None
class TestInjectionPrevention:
"""Test prevention of injection attacks."""
def test_sql_injection_attempts(self):
"""TC-5.2.1: Test SQL injection prevention."""
# Note: Canvas MCP uses API calls, not SQL
# But we should ensure parameters aren't used in string concatenation
injection_attempts = [
"'; DROP TABLE students; --",
"1' OR '1'='1",
"admin'--",
"' OR 1=1--",
]
for attempt in injection_attempts:
# Validate parameter treats these as literal strings
result = validate_parameter("param", attempt, str)
# Should be treated as literal string, not executed
assert isinstance(result, str)
assert result == attempt # Unchanged
def test_command_injection_prevention(self):
"""TC-5.2.1: Test command injection prevention."""
command_injections = [
"; ls -la",
"| cat /etc/passwd",
"&& rm -rf /",
"`whoami`",
"$(whoami)",
]
for injection in command_injections:
result = validate_parameter("param", injection, str)
# Should be treated as literal string
assert isinstance(result, str)
assert result == injection # Commands should not be executed
def test_path_traversal_prevention(self):
"""TC-5.2.2: Test path traversal prevention."""
path_traversals = [
"../../../etc/passwd",
"..\\..\\..\\windows\\system32",
"/etc/passwd",
"C:\\Windows\\System32",
"./../...//..//etc/passwd",
]
for path in path_traversals:
# Verify path traversal patterns are handled safely
result = validate_parameter("path", path, str)
# Implementation should sanitize or reject
assert isinstance(result, str)
def test_xss_injection_attempts(self):
"""TC-5.2.3: Test XSS prevention."""
xss_attempts = [
"<script>alert('XSS')</script>",
"<img src=x onerror=alert('XSS')>",
"javascript:alert('XSS')",
"<iframe src='javascript:alert(1)'>",
"'-alert(1)-'",
]
for xss in xss_attempts:
result = validate_parameter("content", xss, str)
# Should be treated as literal string
# HTML encoding should happen on output, not input
assert isinstance(result, str)
class TestParameterSanitization:
"""Test parameter sanitization."""
def test_whitespace_handling(self):
"""Test whitespace in parameters."""
# Leading/trailing whitespace
result = validate_parameter("param", " value ", str)
# May or may not strip - depends on implementation
assert isinstance(result, str)
def test_unicode_handling(self):
"""Test Unicode character handling."""
unicode_strings = [
"Hello 世界", # Chinese
"Привет мир", # Russian
"مرحبا العالم", # Arabic
"🎉🚀💯", # Emojis
]
for unicode_str in unicode_strings:
result = validate_parameter("text", unicode_str, str)
assert isinstance(result, str)
assert result == unicode_str
def test_length_limits(self):
"""Test extremely long input handling."""
# Very long string
long_string = "A" * 1000000 # 1 million characters
# Should handle gracefully (accept or reject with clear error)
try:
result = validate_parameter("text", long_string, str)
assert isinstance(result, str)
assert len(result) == 1000000
except ValueError:
# Length limit exceeded - acceptable
pass
class TestTypeCoercion:
"""Test type coercion safety."""
def test_string_to_int_coercion(self):
"""Test safe string to integer conversion."""
# Valid integer string
result = validate_parameter("id", "12345", int)
assert result == 12345
assert isinstance(result, int)
# Invalid integer string
with pytest.raises((TypeError, ValueError)):
validate_parameter("id", "not_a_number", int)
def test_string_to_list_coercion(self):
"""Test string to list conversion."""
# Comma-separated string to list
# Implementation depends on validation logic
pass
def test_type_confusion_prevention(self):
"""Test prevention of type confusion attacks."""
# Attempt to confuse type system
confusing_inputs = [
"[object Object]",
"true", # String "true" vs boolean
"null", # String "null" vs None
]
for input_val in confusing_inputs:
# Should maintain type safety - strings stay strings
result = validate_parameter("param", input_val, str)
assert isinstance(result, str)
assert result == input_val
# Dict input should be converted to string
result = validate_parameter("param", {"__proto__": "malicious"}, str)
assert isinstance(result, str)
class TestErrorMessages:
"""Test that error messages don't leak sensitive information."""
def test_validation_errors_safe(self):
"""Verify validation errors don't expose system details."""
try:
validate_parameter("secret_param", None, str)
except Exception as e:
error_msg = str(e)
# Error should be clear but not expose internals
assert "secret_param" in error_msg # Parameter name is ok
# Should not contain file paths or stack traces in message
assert "File" not in error_msg
if __name__ == "__main__":
pytest.main([__file__, "-v"])