""" SQL Injection Security Tests for Context Recall API Tests that the recall API is properly protected against SQL injection attacks. Validates both the input validation layer and the parameterized query layer. """ import unittest import requests from typing import Dict, Any # Import auth utilities for token creation from api.middleware.auth import create_access_token # Test configuration API_BASE_URL = "http://172.16.3.30:8001/api" TEST_USER_EMAIL = "admin@claudetools.local" class TestSQLInjectionSecurity(unittest.TestCase): """Test suite for SQL injection attack prevention.""" @classmethod def setUpClass(cls): """Create test JWT token for authentication.""" # Create token directly without login endpoint cls.token = create_access_token({"sub": TEST_USER_EMAIL}) cls.headers = {"Authorization": f"Bearer {cls.token}"} # SQL Injection Test Cases for search_term parameter def test_sql_injection_search_term_basic_attack(self): """Test basic SQL injection attempt via search_term.""" malicious_input = "' OR '1'='1" response = requests.get( f"{API_BASE_URL}/conversation-contexts/recall", params={"search_term": malicious_input}, headers=self.headers ) # Should reject due to pattern validation (contains single quotes) assert response.status_code == 422, "Failed to reject SQL injection attack" error_detail = response.json()["detail"] assert any("pattern" in str(err).lower() or "match" in str(err).lower() for err in error_detail if isinstance(err, dict)) def test_sql_injection_search_term_union_attack(self): """Test UNION-based SQL injection attempt.""" malicious_input = "' UNION SELECT * FROM users--" response = requests.get( f"{API_BASE_URL}/conversation-contexts/recall", params={"search_term": malicious_input}, headers=self.headers ) # Should reject due to pattern validation assert response.status_code == 422, "Failed to reject UNION attack" def test_sql_injection_search_term_comment_injection(self): """Test comment-based SQL injection.""" malicious_input = "test' --" response = requests.get( f"{API_BASE_URL}/conversation-contexts/recall", params={"search_term": malicious_input}, headers=self.headers ) # Should reject due to pattern validation (contains single quote) assert response.status_code == 422, "Failed to reject comment injection" def test_sql_injection_search_term_semicolon_attack(self): """Test semicolon-based SQL injection for multiple statements.""" malicious_input = "test'; DROP TABLE conversation_contexts;--" response = requests.get( f"{API_BASE_URL}/conversation-contexts/recall", params={"search_term": malicious_input}, headers=self.headers ) # Should reject due to pattern validation (contains semicolon and quotes) assert response.status_code == 422, "Failed to reject DROP TABLE attack" def test_sql_injection_search_term_encoded_attack(self): """Test URL-encoded SQL injection attempt.""" # URL encoding of "' OR 1=1--" malicious_input = "%27%20OR%201%3D1--" response = requests.get( f"{API_BASE_URL}/conversation-contexts/recall", params={"search_term": malicious_input}, headers=self.headers ) # Should reject due to pattern validation after decoding assert response.status_code == 422, "Failed to reject encoded attack" # SQL Injection Test Cases for tags parameter def test_sql_injection_tags_basic_attack(self): """Test SQL injection via tags parameter.""" malicious_tag = "' OR '1'='1" response = requests.get( f"{API_BASE_URL}/conversation-contexts/recall", params={"tags": [malicious_tag]}, headers=self.headers ) # Should reject due to tag validation (contains single quotes and spaces) assert response.status_code == 400, "Failed to reject SQL injection via tags" assert "Invalid tag format" in response.json()["detail"] def test_sql_injection_tags_union_attack(self): """Test UNION attack via tags parameter.""" malicious_tag = "tag' UNION SELECT password FROM users--" response = requests.get( f"{API_BASE_URL}/conversation-contexts/recall", params={"tags": [malicious_tag]}, headers=self.headers ) # Should reject due to tag validation assert response.status_code == 400, "Failed to reject UNION attack via tags" def test_sql_injection_tags_multiple_malicious(self): """Test multiple malicious tags.""" malicious_tags = [ "tag1' OR '1'='1", "tag2'; DROP TABLE tags;--", "tag3' UNION SELECT NULL--" ] response = requests.get( f"{API_BASE_URL}/conversation-contexts/recall", params={"tags": malicious_tags}, headers=self.headers ) # Should reject due to tag validation assert response.status_code == 400, "Failed to reject multiple malicious tags" # Valid Input Tests (should succeed) def test_valid_search_term_alphanumeric(self): """Test that valid alphanumeric search terms work.""" valid_input = "API development" response = requests.get( f"{API_BASE_URL}/conversation-contexts/recall", params={"search_term": valid_input}, headers=self.headers ) # Should succeed assert response.status_code == 200, f"Valid input rejected: {response.text}" data = response.json() assert "contexts" in data assert isinstance(data["contexts"], list) def test_valid_search_term_with_punctuation(self): """Test valid search terms with allowed punctuation.""" valid_input = "database-migration (phase-1)!" response = requests.get( f"{API_BASE_URL}/conversation-contexts/recall", params={"search_term": valid_input}, headers=self.headers ) # Should succeed assert response.status_code == 200, f"Valid input rejected: {response.text}" def test_valid_tags(self): """Test that valid tags work.""" valid_tags = ["api", "database", "phase-1", "test_tag"] response = requests.get( f"{API_BASE_URL}/conversation-contexts/recall", params={"tags": valid_tags}, headers=self.headers ) # Should succeed assert response.status_code == 200, f"Valid tags rejected: {response.text}" data = response.json() assert "contexts" in data # Boundary Tests def test_search_term_max_length(self): """Test search term at maximum allowed length (200 chars).""" valid_input = "a" * 200 response = requests.get( f"{API_BASE_URL}/conversation-contexts/recall", params={"search_term": valid_input}, headers=self.headers ) # Should succeed assert response.status_code == 200, "Max length valid input rejected" def test_search_term_exceeds_max_length(self): """Test search term exceeding maximum length.""" invalid_input = "a" * 201 response = requests.get( f"{API_BASE_URL}/conversation-contexts/recall", params={"search_term": invalid_input}, headers=self.headers ) # Should reject assert response.status_code == 422, "Overlong input not rejected" def test_tags_max_items(self): """Test maximum number of tags (20).""" valid_tags = [f"tag{i}" for i in range(20)] response = requests.get( f"{API_BASE_URL}/conversation-contexts/recall", params={"tags": valid_tags}, headers=self.headers ) # Should succeed assert response.status_code == 200, "Max tags rejected" def test_tags_exceeds_max_items(self): """Test exceeding maximum number of tags.""" invalid_tags = [f"tag{i}" for i in range(21)] response = requests.get( f"{API_BASE_URL}/conversation-contexts/recall", params={"tags": invalid_tags}, headers=self.headers ) # Should reject assert response.status_code == 422, "Too many tags not rejected" # Advanced SQL Injection Techniques def test_sql_injection_hex_encoding(self): """Test hex-encoded SQL injection.""" malicious_input = "0x27204f522031203d2031" # Hex for "' OR 1 = 1" response = requests.get( f"{API_BASE_URL}/conversation-contexts/recall", params={"search_term": malicious_input}, headers=self.headers ) # Pattern allows alphanumeric, so this passes input validation # Should be safe due to parameterized queries assert response.status_code == 200, "Hex encoding caused error" # Verify it's treated as literal search, not executed as SQL data = response.json() assert isinstance(data["contexts"], list) def test_sql_injection_time_based_blind(self): """Test time-based blind SQL injection attempt.""" malicious_input = "' AND SLEEP(5)--" response = requests.get( f"{API_BASE_URL}/conversation-contexts/recall", params={"search_term": malicious_input}, headers=self.headers ) # Should reject due to pattern validation assert response.status_code == 422, "Time-based attack not rejected" def test_sql_injection_stacked_queries(self): """Test stacked query injection.""" malicious_input = "test; DELETE FROM conversation_contexts WHERE 1=1" response = requests.get( f"{API_BASE_URL}/conversation-contexts/recall", params={"search_term": malicious_input}, headers=self.headers ) # Should reject due to pattern validation (semicolon not allowed) assert response.status_code == 422, "Stacked query attack not rejected" # Verify Database Integrity def test_database_not_compromised(self): """Verify database still functions after attack attempts.""" # Simple query to verify database is intact response = requests.get( f"{API_BASE_URL}/conversation-contexts/recall", params={"limit": 5}, headers=self.headers ) assert response.status_code == 200, "Database may be compromised" data = response.json() assert "contexts" in data assert isinstance(data["contexts"], list) def test_fulltext_index_still_works(self): """Verify FULLTEXT index functionality after attacks.""" # Test normal search that should use FULLTEXT index response = requests.get( f"{API_BASE_URL}/conversation-contexts/recall", params={"search_term": "test"}, headers=self.headers ) assert response.status_code == 200, "FULLTEXT search failed" data = response.json() assert isinstance(data["contexts"], list) if __name__ == "__main__": print("=" * 70) print("SQL INJECTION SECURITY TEST SUITE") print("=" * 70) print() print("Testing Context Recall API endpoint security...") print(f"Target: {API_BASE_URL}/conversation-contexts/recall") print() # Run tests unittest.main(verbosity=2)