| """ |
| Authentication endpoint fuzzing harnesses. |
| |
| This module uses Atheris to discover crashes in authentication endpoint |
| parsing and validation code through coverage-guided fuzzing. |
| |
| Fuzzing Targets: |
| - POST /api/auth/login - Login endpoint |
| - POST /api/auth/signup - Signup endpoint |
| - POST /api/auth/mobile/login - Mobile login endpoint |
| |
| Usage: |
| FUZZ_ITERATIONS=10000 pytest tests/fuzzing/test_auth_api_fuzzing.py -v -m fuzzing |
| """ |
|
|
| import os |
| import sys |
| import json |
| import pytest |
| from typing import Dict, Any |
|
|
| |
| 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 fastapi.testclient import TestClient |
| from sqlalchemy.orm import Session |
|
|
| |
| from tests.fuzzing.conftest import ATHERIS_AVAILABLE |
| from tests.e2e_ui.fixtures.database_fixtures import db_session |
| from tests.e2e_ui.fixtures.auth_fixtures import authenticated_user, test_user |
|
|
| from main_api_app import app |
| from core.database import get_db |
|
|
| |
| try: |
| import atheris |
| from atheris import fp |
| except ImportError: |
| ATHERIS_AVAILABLE = False |
| pytest.skip("Atheris not installed - skipping auth endpoint fuzzing", allow_module_level=True) |
|
|
|
|
| |
| |
| |
|
|
| @pytest.mark.fuzzing |
| @pytest.mark.slow |
| @pytest.mark.timeout(300) |
| def test_login_endpoint_fuzzing(db_session: Session): |
| """ |
| Fuzz POST /api/auth/login endpoint. |
| |
| PROPERTY: Login endpoint should not crash on malformed input. |
| STRATEGY: FuzzedDataProvider generates random email/password combinations. |
| INVARIANT: Response status code in [200, 400, 401, 422] (no 500 errors). |
| RADII: 10000 iterations sufficient for login input space (email + password). |
| |
| Fuzzed fields: |
| - email: Random string up to 100 chars |
| - password: Random string up to 100 chars |
| """ |
| |
| app.dependency_overrides[get_db] = lambda: db_session |
|
|
| |
| client = TestClient(app) |
|
|
| def fuzz_one_input(data: bytes): |
| """Fuzzing target for login endpoint.""" |
| try: |
| |
| fdp = fp.FuzzedDataProvider(data) |
|
|
| |
| email = fdp.ConsumeRandomLengthString(100) |
| password = fdp.ConsumeRandomLengthString(100) |
|
|
| |
| response = client.post( |
| "/api/auth/login", |
| json={"email": email, "password": password} |
| ) |
|
|
| |
| assert response.status_code in [200, 400, 401, 422], \ |
| f"Unexpected status {response.status_code}: {response.text[:200]}" |
|
|
| except (ValueError, json.decoder.JSONDecodeError) as e: |
| |
| pass |
| except Exception as e: |
| |
| raise Exception(f"Login endpoint crashed: {e}") |
|
|
| |
| atheris.Setup( |
| sys.argv + [atheris.FuzzedDataProviderFlag], |
| fuzz_one_input |
| ) |
|
|
| |
| iterations = int(os.getenv("FUZZ_ITERATIONS", "10000")) |
| atheris.Fuzz(iterations=iterations) |
|
|
| |
| app.dependency_overrides = {} |
|
|
|
|
| |
| |
| |
|
|
| @pytest.mark.fuzzing |
| @pytest.mark.slow |
| @pytest.mark.timeout(300) |
| def test_signup_endpoint_fuzzing(db_session: Session): |
| """ |
| Fuzz POST /api/auth/signup endpoint. |
| |
| PROPERTY: Signup endpoint should not crash on malformed input. |
| STRATEGY: FuzzedDataProvider generates random signup payloads. |
| INVARIANT: Response status code in [200, 400, 409, 422] (no 500 errors). |
| RADII: 10000 iterations sufficient for signup input space (email + password + confirm + name). |
| |
| Fuzzed fields: |
| - email: Random string up to 100 chars |
| - password: Random string up to 100 chars |
| - confirm_password: Random string up to 100 chars |
| - name: Random string up to 100 chars |
| """ |
| import json |
|
|
| |
| app.dependency_overrides[get_db] = lambda: db_session |
| client = TestClient(app) |
|
|
| def fuzz_one_input(data: bytes): |
| """Fuzzing target for signup endpoint.""" |
| try: |
| fdp = fp.FuzzedDataProvider(data) |
|
|
| |
| email = fdp.ConsumeRandomLengthString(100) |
| password = fdp.ConsumeRandomLengthString(100) |
| confirm_password = fdp.ConsumeRandomLengthString(100) |
| name = fdp.ConsumeRandomLengthString(100) |
|
|
| |
| response = client.post( |
| "/api/auth/signup", |
| json={ |
| "email": email, |
| "password": password, |
| "confirm_password": confirm_password, |
| "name": name |
| } |
| ) |
|
|
| |
| assert response.status_code in [200, 400, 409, 422], \ |
| f"Unexpected status {response.status_code}: {response.text[:200]}" |
|
|
| except (ValueError, json.decoder.JSONDecodeError): |
| pass |
| except Exception as e: |
| raise Exception(f"Signup endpoint crashed: {e}") |
|
|
| atheris.Setup( |
| sys.argv + [atheris.FuzzedDataProviderFlag], |
| fuzz_one_input |
| ) |
|
|
| iterations = int(os.getenv("FUZZ_ITERATIONS", "10000")) |
| atheris.Fuzz(iterations=iterations) |
|
|
| app.dependency_overrides = {} |
|
|
|
|
| |
| |
| |
|
|
| @pytest.mark.fuzzing |
| @pytest.mark.slow |
| @pytest.mark.timeout(300) |
| def test_mobile_login_fuzzing(db_session: Session, authenticated_user): |
| """ |
| Fuzz POST /api/auth/mobile/login endpoint. |
| |
| PROPERTY: Mobile login endpoint should not crash on malformed device tokens. |
| STRATEGY: FuzzedDataProvider generates random device tokens and platforms. |
| INVARIANT: Response status code in [200, 400, 401, 422] (no 500 errors). |
| RADII: 10000 iterations sufficient for device token/platform space. |
| |
| Fuzzed fields: |
| - device_token: Random string up to 100 chars |
| - platform: Random string up to 20 chars (ios, android, or random) |
| - device_info: Random dictionary with nested fuzzed data |
| |
| Uses authenticated_user fixture for valid credentials baseline. |
| """ |
| import json |
|
|
| |
| app.dependency_overrides[get_db] = lambda: db_session |
| client = TestClient(app) |
|
|
| |
| user, _ = authenticated_user |
| valid_email = user.email |
|
|
| def fuzz_one_input(data: bytes): |
| """Fuzzing target for mobile login endpoint.""" |
| try: |
| fdp = fp.FuzzedDataProvider(data) |
|
|
| |
| device_token = fdp.ConsumeRandomLengthString(100) |
| platform = fdp.ConsumeRandomLengthString(20) |
|
|
| |
| device_info: Dict[str, Any] = {} |
| if fdp.ConsumeBool(): |
| device_info["device_id"] = fdp.ConsumeRandomLengthString(50) |
| if fdp.ConsumeBool(): |
| device_info["app_version"] = fdp.ConsumeRandomLengthString(20) |
| if fdp.ConsumeBool(): |
| device_info["os_version"] = fdp.ConsumeRandomLengthString(20) |
|
|
| |
| response = client.post( |
| "/api/auth/mobile/login", |
| json={ |
| "email": valid_email, |
| "password": "TestPassword123!", |
| "device_token": device_token, |
| "platform": platform, |
| "device_info": device_info |
| } |
| ) |
|
|
| |
| assert response.status_code in [200, 400, 401, 422], \ |
| f"Unexpected status {response.status_code}: {response.text[:200]}" |
|
|
| except (ValueError, json.decoder.JSONDecodeError): |
| pass |
| except Exception as e: |
| raise Exception(f"Mobile login endpoint crashed: {e}") |
|
|
| atheris.Setup( |
| sys.argv + [atheris.FuzzedDataProviderFlag], |
| fuzz_one_input |
| ) |
|
|
| iterations = int(os.getenv("FUZZ_ITERATIONS", "10000")) |
| atheris.Fuzz(iterations=iterations) |
|
|
| app.dependency_overrides = {} |
|
|