| """ |
| 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 |
|
|
| |
| 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 |
|
|
| |
| from tests.e2e_ui.fixtures.auth_fixtures import authenticated_user |
| from tests.e2e_ui.fixtures.database_fixtures import db_session |
|
|
| |
| try: |
| import atheris |
| from atheris import fp |
| ATHERIS_AVAILABLE = True |
| except ImportError: |
| ATHERIS_AVAILABLE = False |
|
|
|
|
| |
| |
| |
|
|
| @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 |
|
|
| |
| app.dependency_overrides[get_db] = lambda: db_session |
|
|
| |
| client = TestClient(app) |
|
|
| |
| headers = {"Authorization": f"Bearer {token}"} |
|
|
| |
| 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) |
|
|
| |
| 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: |
| |
| canvas_type = "'; DROP TABLE canvas; --" |
| elif canvas_type_option == 3: |
| |
| canvas_type = "invalid_type_" + fdp.ConsumeRandomLengthString(20) |
| elif canvas_type_option == 4: |
| |
| canvas_type = "<script>alert('xss')</script>" |
| else: |
| |
| canvas_type = fdp.PickValueInList(valid_canvas_types) |
|
|
| |
| 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: |
| |
| value = "" |
| elif value_option == 2: |
| |
| value = fdp.ConsumeRandomLengthString(100) |
| elif value_option == 3: |
| |
| value = "A" * 10000000 |
| elif value_option == 4: |
| |
| value = fdp.ConsumeIntInRange(-1000000, 1000000) |
| elif value_option == 5: |
| |
| value = { |
| "level1": { |
| "level2": { |
| "level3": fdp.ConsumeRandomLengthString(50) |
| } |
| } |
| } |
| else: |
| |
| value = [ |
| fdp.ConsumeRandomLengthString(20), |
| fdp.ConsumeIntInRange(0, 1000), |
| None, |
| {"nested": fdp.ConsumeRandomLengthString(10)} |
| ] |
|
|
| canvas_data[key] = value |
|
|
| |
| 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) |
|
|
| |
| payload = { |
| "canvas_type": canvas_type, |
| "canvas_data": canvas_data |
| } |
| if agent_id is not None: |
| payload["agent_id"] = agent_id |
|
|
| |
| response = client.post( |
| "/api/canvas/present", |
| json=payload, |
| headers=headers |
| ) |
|
|
| |
| |
| 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: |
| |
| pass |
| except Exception as e: |
| |
| raise Exception(f"Crash in canvas present fuzzing: {e}") |
|
|
| |
| iterations = int(os.getenv("FUZZ_ITERATIONS", "10000")) |
| atheris.Setup(sys.argv, []) |
| atheris.Fuzz(fuzz_one_input, iterations=iterations) |
|
|
|
|
| |
| |
| |
|
|
| @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 |
|
|
| |
| 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) |
|
|
| |
| 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" |
| elif canvas_id_option == 3: |
| canvas_id = "A" * 10000 |
| else: |
| canvas_id = fdp.ConsumeRandomLengthString(100) |
|
|
| |
| 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 |
| elif value_option == 1: |
| value = fdp.ConsumeRandomLengthString(1000) |
| else: |
| |
| value = { |
| "nested_key": fdp.ConsumeRandomLengthString(50), |
| "nested_value": fdp.ConsumeIntInRange(0, 1000) |
| } |
|
|
| update_data[key] = value |
|
|
| |
| response = client.post( |
| f"/api/canvas/{canvas_id}/update", |
| json={"update_data": update_data}, |
| headers=headers |
| ) |
|
|
| |
| 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) |
|
|
|
|
| |
| |
| |
|
|
| @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 |
|
|
| |
| 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) |
|
|
| |
| 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 = "<img src=x onerror=alert('xss')>" |
| else: |
| canvas_id = fdp.ConsumeRandomLengthString(100) |
|
|
| |
| response = client.post( |
| f"/api/canvas/{canvas_id}/close", |
| headers=headers |
| ) |
|
|
| |
| 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) |
|
|
|
|
| |
| |
| |
|
|
| @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 |
|
|
| |
| 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) |
|
|
| |
| canvas_id_option = fdp.ConsumeIntInRange(0, 2) |
| if canvas_id_option == 0: |
| canvas_id = "'; DELETE FROM canvas_audit; --" |
| else: |
| canvas_id = fdp.ConsumeRandomLengthString(50) |
|
|
| |
| 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: |
| |
| action_id = "action_1'; DROP TABLE users; --" |
| else: |
| action_id = fdp.ConsumeRandomLengthString(50) |
|
|
| |
| 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: |
| |
| value = "'; DROP TABLE users; --" |
| elif value_option == 2: |
| |
| value = "<script>alert('xss')</script>" |
| elif value_option == 3: |
| |
| value = "test\x00\x00\x00" |
| elif value_option == 4: |
| |
| value = "../../../../etc/passwd" |
| else: |
| value = fdp.ConsumeRandomLengthString(100) |
|
|
| action_parameters[key] = value |
|
|
| |
| payload = { |
| "action_id": action_id, |
| "action_parameters": action_parameters |
| } |
|
|
| |
| response = client.post( |
| f"/api/canvas/{canvas_id}/execute", |
| json=payload, |
| headers=headers |
| ) |
|
|
| |
| 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) |
|
|