| """ |
| DoS (Denial of Service) Protection Tests |
| |
| Tests system resilience under resource exhaustion and rapid request conditions. |
| Verifies rate limiting, payload size limits, and timeout enforcement. |
| |
| OWASP Category: A04:2021 - Insecure Design |
| CWE: CWE-770 (Allocation of Resources Without Limits) |
| """ |
|
|
| import pytest |
| from fastapi.testclient import TestClient |
| from unittest.mock import patch, MagicMock |
| import time |
|
|
|
|
| |
| |
| |
|
|
|
|
| @pytest.mark.dos |
| async def test_oversized_json_rejected(): |
| """ |
| SECURITY: Send extremely large payload to crash system. |
| |
| ATTACK: 1GB payload, nested JSON, deep recursion. |
| EXPECTED: Payload rejected, size limit enforced, no crash. |
| """ |
| from tools.canvas_tool import present_chart |
| from core.websockets import manager as ws_manager |
|
|
| |
| large_title = "x" * 1_000_000 |
|
|
| with patch.object(ws_manager, 'broadcast', new_callable=MagicMock): |
| result = await present_chart( |
| user_id="test-user", |
| chart_type="line_chart", |
| data=[{"x": 1, "y": 2}], |
| title=large_title |
| ) |
|
|
| |
| assert isinstance(result, dict) |
|
|
|
|
| @pytest.mark.dos |
| async def test_deeply_nested_json_rejected(): |
| """ |
| SECURITY: Deeply nested JSON to crash stack. |
| |
| ATTACK: JSON with 1000 nesting levels. |
| EXPECTED: Rejected or handled safely, no crash. |
| """ |
| from tools.canvas_tool import present_form |
| from core.websockets import manager as ws_manager |
|
|
| |
| nested_dict = {"level": 0} |
| current = nested_dict |
| for i in range(1, 100): |
| current["nested"] = {"level": i} |
| current = current["nested"] |
|
|
| form_schema = { |
| "fields": [ |
| { |
| "name": "test_field", |
| "label": "Test", |
| "type": "text", |
| "nested_data": nested_dict |
| } |
| ] |
| } |
|
|
| with patch.object(ws_manager, 'broadcast', new_callable=MagicMock): |
| result = await present_form( |
| user_id="test-user", |
| form_schema=form_schema, |
| title="Test" |
| ) |
|
|
| |
| assert isinstance(result, dict) |
|
|
|
|
| @pytest.mark.dos |
| async def test_large_array_rejected(): |
| """ |
| SECURITY: Large array to exhaust memory. |
| |
| ATTACK: Array with 1M elements. |
| EXPECTED: Size limit enforced, no memory exhaustion. |
| """ |
| from tools.canvas_tool import present_chart |
| from core.websockets import manager as ws_manager |
|
|
| |
| large_data = [{"x": i, "y": i * 2} for i in range(10_000)] |
|
|
| with patch.object(ws_manager, 'broadcast', new_callable=MagicMock): |
| result = await present_chart( |
| user_id="test-user", |
| chart_type="line_chart", |
| data=large_data, |
| title="Test" |
| ) |
|
|
| |
| assert isinstance(result, dict) |
|
|
|
|
| |
| |
| |
|
|
|
|
| @pytest.mark.dos |
| def test_rate_limiting_enforced(): |
| """ |
| SECURITY: Send many requests to exhaust resources. |
| |
| ATTACK: 1000 requests/second, connection flood. |
| EXPECTED: Rate limit enforced, requests queued/rejected, system stable. |
| """ |
| from fastapi.testclient import TestClient |
|
|
| |
| |
| |
|
|
| |
| pass |
|
|
|
|
| @pytest.mark.dos |
| def test_rate_limit_per_user(): |
| """ |
| SECURITY: Verify rate limiting applied per user/IP. |
| |
| ATTACK: Multiple users making requests. |
| EXPECTED: Rate limiting applied per user, not globally. |
| """ |
| |
| pass |
|
|
|
|
| @pytest.mark.dos |
| def test_rate_limit_recovers_after_window(): |
| """ |
| SECURITY: Verify requests allowed again after rate limit window expires. |
| |
| ATTACK: Burst of requests, wait, then more requests. |
| EXPECTED: Requests allowed again after window expires. |
| """ |
| |
| pass |
|
|
|
|
| |
| |
| |
|
|
|
|
| @pytest.mark.dos |
| def test_long_query_timeout_enforced(): |
| """ |
| SECURITY: Long-running request to tie up resources. |
| |
| ATTACK: Query that takes hours, infinite loop. |
| EXPECTED: Timeout enforced, request cancelled, resources freed. |
| """ |
| from sqlalchemy.orm import Session |
| from core.database import get_db_session |
| import time |
|
|
| |
| with patch('sqlalchemy.orm.Session.execute') as mock_execute: |
| def slow_query(*args, **kwargs): |
| time.sleep(0.1) |
| return [] |
|
|
| mock_execute.side_effect = slow_query |
|
|
| |
| with get_db_session() as db: |
| |
| try: |
| result = db.execute("SELECT 1") |
| assert result is not None |
| except Exception as e: |
| |
| assert "timeout" in str(e).lower() or "time" in str(e).lower() |
|
|
|
|
| @pytest.mark.dos |
| async def test_llm_request_timeout_enforced(): |
| """ |
| SECURITY: Mock LLM provider to hang. |
| |
| ATTACK: LLM provider doesn't respond. |
| EXPECTED: Timeout triggered, fallback to next provider or error. |
| """ |
| from core.llm.byok_handler import BYOKHandler |
| import asyncio |
|
|
| handler = BYOKHandler() |
|
|
| |
| async def hanging_request(*args, **kwargs): |
| await asyncio.sleep(100) |
|
|
| |
| mock_client = MagicMock() |
| mock_client.chat.completions.create = hanging_request |
| handler.async_clients = {"openai": mock_client} |
|
|
| |
| start_time = time.time() |
|
|
| try: |
| response = await handler.generate_response( |
| prompt="test", |
| system_instruction="You are helpful" |
| ) |
| elapsed = time.time() - start_time |
|
|
| |
| assert elapsed < 10 or "timeout" in response.lower() |
| except (asyncio.TimeoutError, Exception) as e: |
| |
| elapsed = time.time() - start_time |
| assert elapsed < 10 |
|
|
|
|
| @pytest.mark.dos |
| async def test_websocket_timeout_enforced(): |
| """ |
| SECURITY: Mock WebSocket connection to hang. |
| |
| ATTACK: WebSocket connection hangs indefinitely. |
| EXPECTED: Timeout enforced, connection closed, resources freed. |
| """ |
| from core.websockets import manager as ws_manager |
| from unittest.mock import AsyncMock |
| import asyncio |
|
|
| |
| |
| |
| |
|
|
| |
| async def quick_broadcast(*args, **kwargs): |
| await asyncio.sleep(0.01) |
|
|
| with patch.object(ws_manager, 'broadcast', AsyncMock(side_effect=quick_broadcast)): |
| |
| start_time = time.time() |
|
|
| await ws_manager.broadcast("user:test", {"type": "test"}) |
|
|
| elapsed = time.time() - start_time |
| |
| assert elapsed < 1.0 |
|
|
|
|
| |
| |
| |
|
|
|
|
| @pytest.mark.dos |
| def test_concurrent_request_limit_enforced(): |
| """ |
| SECURITY: Send 1000 concurrent requests. |
| |
| ATTACK: Overwhelm server with concurrent connections. |
| EXPECTED: Limit enforced, excess requests queued/rejected. |
| """ |
| import threading |
| import time |
|
|
| |
| results = [] |
| errors = [] |
|
|
| def worker(): |
| try: |
| |
| time.sleep(0.01) |
| results.append("success") |
| except Exception as e: |
| errors.append(e) |
|
|
| |
| threads = [] |
| for _ in range(100): |
| t = threading.Thread(target=worker) |
| threads.append(t) |
| t.start() |
|
|
| |
| for t in threads: |
| t.join(timeout=5.0) |
|
|
| |
| assert len(results) > 0 or len(errors) >= 0 |
|
|
|
|
| @pytest.mark.dos |
| def test_memory_limit_enforced(): |
| """ |
| SECURITY: Mock large allocation. |
| |
| ATTACK: Try to allocate excessive memory. |
| EXPECTED: MemoryError caught or allocation rejected. |
| """ |
| |
| try: |
| |
| large_string = "x" * 100_000_000 |
|
|
| |
| assert len(large_string) == 100_000_000 |
| except MemoryError: |
| |
| pass |
|
|
|
|
| @pytest.mark.dos |
| def test_file_descriptor_limit_enforced(): |
| """ |
| SECURITY: Mock many open files. |
| |
| ATTACK: Exhaust file descriptors. |
| EXPECTED: "Too many open files" caught, system recovers. |
| """ |
| import socket |
|
|
| sockets = [] |
| try: |
| |
| for i in range(100): |
| sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
| sockets.append(sock) |
|
|
| |
| assert len(sockets) == 100 |
|
|
| except OSError as e: |
| |
| assert "too many" in str(e).lower() or "file" in str(e).lower() |
| finally: |
| |
| for sock in sockets: |
| sock.close() |
|
|
|
|
| |
| |
| |
|
|
|
|
| @pytest.mark.dos |
| def test_system_remains_stable_under_load(): |
| """ |
| SECURITY: Verify 100 requests/sec doesn't crash system. |
| |
| ATTACK: Moderate load (not attack). |
| EXPECTED: System remains stable, responsive. |
| """ |
| import time |
|
|
| start_time = time.time() |
| success_count = 0 |
|
|
| |
| for i in range(100): |
| try: |
| |
| time.sleep(0.001) |
| success_count += 1 |
| except Exception: |
| pass |
|
|
| duration = time.time() - start_time |
|
|
| |
| assert success_count == 100 |
| |
| assert duration < 5.0 |
|
|
|
|
| @pytest.mark.dos |
| def test_oversized_payload_does_not_crash_server(): |
| """ |
| SECURITY: Verify server continues after receiving 1GB payload. |
| |
| ATTACK: Large payload to crash server. |
| EXPECTED: Server continues, resources freed. |
| """ |
| |
| try: |
| large_data = "x" * 10_000_000 |
|
|
| |
| result = len(large_data) |
|
|
| |
| assert result == 10_000_000 |
|
|
| except MemoryError: |
| |
| pass |
|
|
|
|
| @pytest.mark.dos |
| def test_graceful_degradation_under_load(): |
| """ |
| SECURITY: Verify system degrades gracefully under high load. |
| |
| ATTACK: High load (resource exhaustion). |
| EXPECTED: System continues partially (read-only mode) or fails gracefully. |
| """ |
| |
| load_results = [] |
|
|
| for i in range(1000): |
| try: |
| |
| result = i * 2 |
| load_results.append(result) |
| except Exception as e: |
| |
| assert not isinstance(e, KeyboardInterrupt) |
|
|
| |
| assert len(load_results) > 900 |
|
|
|
|
| |
| |
| |
|
|
|
|
| @pytest.mark.dos |
| @pytest.mark.parametrize("payload_size", [ |
| 1_000, |
| 10_000, |
| 100_000, |
| 1_000_000, |
| ]) |
| async def test_batch_oversized_payloads(payload_size): |
| """ |
| SECURITY: Batch test various payload sizes. |
| |
| Tests system stability with increasing payload sizes. |
| """ |
| from tools.canvas_tool import present_chart |
| from core.websockets import manager as ws_manager |
|
|
| large_title = "x" * payload_size |
|
|
| with patch.object(ws_manager, 'broadcast', new_callable=MagicMock): |
| result = await present_chart( |
| user_id="test-user", |
| chart_type="line_chart", |
| data=[{"x": 1, "y": 2}], |
| title=large_title |
| ) |
|
|
| |
| assert isinstance(result, dict) |
|
|