""" Canvas presentation fuzzing harness for discovering crashes in canvas state parsing. This module uses Atheris to fuzz canvas presentation endpoints: - POST /api/canvas/present - Canvas presentation with canvas_data - POST /api/canvas/{id}/update - Canvas state updates - POST /api/canvas/{id}/close - Canvas closure - POST /api/canvas/{id}/execute - Canvas action execution Target: Canvas state parsing/validation code crashes Coverage: SQL injection, XSS, null bytes, huge inputs, malformed JSON, nested structures """ import os import sys import pytest from fastapi.testclient import TestClient from sqlalchemy.orm import Session # Add backend to path for imports backend_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) if backend_dir not in sys.path: sys.path.insert(0, backend_dir) from core.database import get_db from main_api_app import app # Import existing fixtures (FUZZ-02: reuse to avoid duplication) from tests.e2e_ui.fixtures.auth_fixtures import authenticated_user from tests.e2e_ui.fixtures.database_fixtures import db_session # Try to import Atheris (graceful degradation) try: import atheris from atheris import fp ATHERIS_AVAILABLE = True except ImportError: ATHERIS_AVAILABLE = False # ============================================================================ # CANVAS PRESENT FUZZING (POST /api/canvas/present) # ============================================================================ @pytest.mark.fuzzing @pytest.mark.slow @pytest.mark.timeout(300) def test_canvas_present_fuzzing(db_session: Session, authenticated_user): """ Fuzz canvas presentation endpoint (POST /api/canvas/present). Target crashes in: - Canvas type validation - Canvas data JSON parsing - Nested structure handling - Agent ID parsing/validation Edge cases: - Canvas type: None, empty, invalid types, SQL injection - Canvas data: None, empty dict, huge strings (10MB+), nested cyclical references - Agent ID: Invalid UUIDs, SQL injection, XSS - Malformed JSON in canvas_data Canvas types (7 canonical): - chart, markdown, form, sheet, terminal, email, coding Args: db_session: Isolated test database session authenticated_user: (user, token) tuple for JWT auth """ if not ATHERIS_AVAILABLE: pytest.skip("Atheris not installed - fuzzing test skipped") user, token = authenticated_user # Override database to use isolated test session app.dependency_overrides[get_db] = lambda: db_session # Create TestClient with isolated database client = TestClient(app) # Authorization headers headers = {"Authorization": f"Bearer {token}"} # Valid canvas types for sampling valid_canvas_types = ["chart", "markdown", "form", "sheet", "terminal", "email", "coding"] def fuzz_one_input(data: bytes): """Fuzz canvas present endpoint with mutated input. Args: data: Random bytes from Atheris fuzzer Raises: Exception: Crash discovered (Atheris catches this) """ try: fdp = fp.FuzzedDataProvider(data) # Fuzz canvas_type with edge cases canvas_type_option = fdp.ConsumeIntInRange(0, 5) if canvas_type_option == 0: canvas_type = None elif canvas_type_option == 1: canvas_type = "" elif canvas_type_option == 2: # SQL injection canvas_type = "'; DROP TABLE canvas; --" elif canvas_type_option == 3: # Invalid type canvas_type = "invalid_type_" + fdp.ConsumeRandomLengthString(20) elif canvas_type_option == 4: # XSS attempt canvas_type = "" else: # Valid canvas type canvas_type = fdp.PickValueInList(valid_canvas_types) # Fuzz canvas_data dict (0-20 keys, nested structures, huge values) num_keys = fdp.ConsumeIntInRange(0, 20) canvas_data = {} for i in range(num_keys): key = fdp.ConsumeRandomLengthString(30) value_option = fdp.ConsumeIntInRange(0, 6) if value_option == 0: value = None elif value_option == 1: # Empty string value = "" elif value_option == 2: # Normal string value = fdp.ConsumeRandomLengthString(100) elif value_option == 3: # Huge string (potential DoS) value = "A" * 10000000 # 10MB elif value_option == 4: # Integer value = fdp.ConsumeIntInRange(-1000000, 1000000) elif value_option == 5: # Nested dict (deep structure) value = { "level1": { "level2": { "level3": fdp.ConsumeRandomLengthString(50) } } } else: # List with mixed types value = [ fdp.ConsumeRandomLengthString(20), fdp.ConsumeIntInRange(0, 1000), None, {"nested": fdp.ConsumeRandomLengthString(10)} ] canvas_data[key] = value # Fuzz agent_id agent_id_option = fdp.ConsumeIntInRange(0, 3) if agent_id_option == 0: agent_id = None elif agent_id_option == 1: agent_id = "'; DROP TABLE agents; --" else: agent_id = fdp.ConsumeRandomLengthString(50) # Build request payload payload = { "canvas_type": canvas_type, "canvas_data": canvas_data } if agent_id is not None: payload["agent_id"] = agent_id # Make request with fuzzed data response = client.post( "/api/canvas/present", json=payload, headers=headers ) # Assert acceptable status codes (no crashes = 500 errors) # 200: Success, 400: Bad request, 401: Unauthorized, 422: Validation error assert response.status_code in [200, 400, 401, 422], \ f"Unexpected status code {response.status_code}: {response.text}" except (ValueError, KeyError, IndexError, AttributeError) as e: # Expected: parsing errors from malformed input are OK pass except Exception as e: # Unexpected: crash discovered raise Exception(f"Crash in canvas present fuzzing: {e}") # Run Atheris fuzzing iterations = int(os.getenv("FUZZ_ITERATIONS", "10000")) atheris.Setup(sys.argv, []) atheris.Fuzz(fuzz_one_input, iterations=iterations) # ============================================================================ # CANVAS UPDATE FUZZING (POST /api/canvas/{id}/update) # ============================================================================ @pytest.mark.fuzzing @pytest.mark.slow @pytest.mark.timeout(300) def test_canvas_update_fuzzing(db_session: Session, authenticated_user): """ Fuzz canvas update endpoint (POST /api/canvas/{id}/update). Target crashes in: - Canvas ID parsing/validation - Update data merging logic - Partial update handling - Null value handling Edge cases: - Canvas ID: None, empty, SQL injection, huge length - Update data: Partial updates, full replacements, null values - Malformed JSON in update_data Args: db_session: Isolated test database session authenticated_user: (user, token) tuple for JWT auth """ if not ATHERIS_AVAILABLE: pytest.skip("Atheris not installed - fuzzing test skipped") user, token = authenticated_user # Override database app.dependency_overrides[get_db] = lambda: db_session client = TestClient(app) headers = {"Authorization": f"Bearer {token}"} def fuzz_one_input(data: bytes): """Fuzz canvas update endpoint.""" try: fdp = fp.FuzzedDataProvider(data) # Fuzz canvas_id canvas_id_option = fdp.ConsumeIntInRange(0, 4) if canvas_id_option == 0: canvas_id = "" elif canvas_id_option == 1: canvas_id = "'; DELETE FROM canvas; --" elif canvas_id_option == 2: canvas_id = "\x00\x00\x00" # Null bytes elif canvas_id_option == 3: canvas_id = "A" * 10000 # Huge input else: canvas_id = fdp.ConsumeRandomLengthString(100) # Fuzz update_data dict num_keys = fdp.ConsumeIntInRange(0, 15) update_data = {} for i in range(num_keys): key = fdp.ConsumeRandomLengthString(30) value_option = fdp.ConsumeIntInRange(0, 3) if value_option == 0: value = None # Test null value handling elif value_option == 1: value = fdp.ConsumeRandomLengthString(1000) else: # Nested structure value = { "nested_key": fdp.ConsumeRandomLengthString(50), "nested_value": fdp.ConsumeIntInRange(0, 1000) } update_data[key] = value # Make request response = client.post( f"/api/canvas/{canvas_id}/update", json={"update_data": update_data}, headers=headers ) # Assert acceptable status codes assert response.status_code in [200, 400, 404, 422], \ f"Unexpected status code {response.status_code}: {response.text}" except (ValueError, KeyError): pass except Exception as e: raise Exception(f"Crash in canvas update fuzzing: {e}") iterations = int(os.getenv("FUZZ_ITERATIONS", "10000")) atheris.Setup(sys.argv, []) atheris.Fuzz(fuzz_one_input, iterations=iterations) # ============================================================================ # CANVAS CLOSE FUZZING (POST /api/canvas/{id}/close) # ============================================================================ @pytest.mark.fuzzing @pytest.mark.slow @pytest.mark.timeout(300) def test_canvas_close_fuzzing(db_session: Session, authenticated_user): """ Fuzz canvas close endpoint (POST /api/canvas/{id}/close). Target crashes in: - Canvas ID parsing/validation - Close operation logic - State cleanup Edge cases: - Canvas ID: SQL injection, XSS, null bytes - Non-existent canvas IDs - Already closed canvases Args: db_session: Isolated test database session authenticated_user: (user, token) tuple for JWT auth """ if not ATHERIS_AVAILABLE: pytest.skip("Atheris not installed - fuzzing test skipped") user, token = authenticated_user # Override database app.dependency_overrides[get_db] = lambda: db_session client = TestClient(app) headers = {"Authorization": f"Bearer {token}"} def fuzz_one_input(data: bytes): """Fuzz canvas close endpoint.""" try: fdp = fp.FuzzedDataProvider(data) # Fuzz canvas_id canvas_id_option = fdp.ConsumeIntInRange(0, 3) if canvas_id_option == 0: canvas_id = "'; DROP TABLE canvas_audit; --" elif canvas_id_option == 1: canvas_id = "" else: canvas_id = fdp.ConsumeRandomLengthString(100) # Make request response = client.post( f"/api/canvas/{canvas_id}/close", headers=headers ) # Assert acceptable status codes assert response.status_code in [200, 400, 404, 422], \ f"Unexpected status code {response.status_code}: {response.text}" except (ValueError, KeyError): pass except Exception as e: raise Exception(f"Crash in canvas close fuzzing: {e}") iterations = int(os.getenv("FUZZ_ITERATIONS", "10000")) atheris.Setup(sys.argv, []) atheris.Fuzz(fuzz_one_input, iterations=iterations) # ============================================================================ # CANVAS EXECUTE FUZZING (POST /api/canvas/{id}/execute) # ============================================================================ @pytest.mark.fuzzing @pytest.mark.slow @pytest.mark.timeout(300) def test_canvas_execute_fuzzing(db_session: Session, authenticated_user): """ Fuzz canvas action execution endpoint (POST /api/canvas/{id}/execute). Target crashes in: - Canvas ID parsing/validation - Action ID parsing - Action parameter handling - Form submission logic Edge cases: - Canvas ID: SQL injection, XSS, null bytes - Action ID: Invalid IDs, SQL injection - Action parameters: Malicious payloads (SQL injection, XSS, null bytes) - Form submission with huge payloads Args: db_session: Isolated test database session authenticated_user: (user, token) tuple for JWT auth """ if not ATHERIS_AVAILABLE: pytest.skip("Atheris not installed - fuzzing test skipped") user, token = authenticated_user # Override database app.dependency_overrides[get_db] = lambda: db_session client = TestClient(app) headers = {"Authorization": f"Bearer {token}"} def fuzz_one_input(data: bytes): """Fuzz canvas execute endpoint with malicious payloads.""" try: fdp = fp.FuzzedDataProvider(data) # Fuzz canvas_id canvas_id_option = fdp.ConsumeIntInRange(0, 2) if canvas_id_option == 0: canvas_id = "'; DELETE FROM canvas_audit; --" else: canvas_id = fdp.ConsumeRandomLengthString(50) # Fuzz action_id action_id_option = fdp.ConsumeIntInRange(0, 3) if action_id_option == 0: action_id = None elif action_id_option == 1: action_id = "" elif action_id_option == 2: # SQL injection in action_id action_id = "action_1'; DROP TABLE users; --" else: action_id = fdp.ConsumeRandomLengthString(50) # Fuzz action_parameters dict (malicious payloads) num_params = fdp.ConsumeIntInRange(0, 10) action_parameters = {} for i in range(num_params): key = fdp.ConsumeRandomLengthString(20) value_option = fdp.ConsumeIntInRange(0, 5) if value_option == 0: value = None elif value_option == 1: # SQL injection payload value = "'; DROP TABLE users; --" elif value_option == 2: # XSS payload value = "" elif value_option == 3: # Null bytes value = "test\x00\x00\x00" elif value_option == 4: # Path traversal value = "../../../../etc/passwd" else: value = fdp.ConsumeRandomLengthString(100) action_parameters[key] = value # Build request payload payload = { "action_id": action_id, "action_parameters": action_parameters } # Make request response = client.post( f"/api/canvas/{canvas_id}/execute", json=payload, headers=headers ) # Assert acceptable status codes assert response.status_code in [200, 400, 404, 422], \ f"Unexpected status code {response.status_code}: {response.text}" except (ValueError, KeyError): pass except Exception as e: raise Exception(f"Crash in canvas execute fuzzing: {e}") iterations = int(os.getenv("FUZZ_ITERATIONS", "10000")) atheris.Setup(sys.argv, []) atheris.Fuzz(fuzz_one_input, iterations=iterations)