From e8ca10f1b7c3325550564a48bb33726d9dbc5635 Mon Sep 17 00:00:00 2001 From: Olivier Date: Mon, 18 May 2026 18:16:08 +0200 Subject: [PATCH] fix: cap /api/discovery/ping at 4096 IPs and fix test suite - Add MAX_PING_IPS=4096 constant and validate list size in PingRequest before spawning futures, returning 422 on overflow - Add test_ping_too_many_ips_rejected to cover the new cap - Pin httpx<0.28 in requirements-test.txt (0.28 broke TestClient API) - Fix reset_db fixture to set a known admin password regardless of INITIAL_ADMIN_PASSWORD env var (was causing 401 on all auth tests) Co-Authored-By: Claude Sonnet 4.6 --- backend/requirements-test.txt | 2 +- backend/routers/discovery.py | 3 +++ backend/tests/test_validation.py | 17 ++++++++++++++++- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/backend/requirements-test.txt b/backend/requirements-test.txt index ab7351d..46c7482 100644 --- a/backend/requirements-test.txt +++ b/backend/requirements-test.txt @@ -1,2 +1,2 @@ pytest>=7.4 -httpx>=0.25 +httpx>=0.25,<0.28 diff --git a/backend/routers/discovery.py b/backend/routers/discovery.py index b10d874..b5101d5 100644 --- a/backend/routers/discovery.py +++ b/backend/routers/discovery.py @@ -16,6 +16,7 @@ router = APIRouter() MAX_HOSTS_PER_TARGET = 1024 # refuse les /21 et plus larges MAX_HOSTS_TOTAL = 4096 # cap global sur l'ensemble des targets +MAX_PING_IPS = 4096 # cap sur /api/discovery/ping _ENV_DNS = os.environ.get("DNS_SERVER", "").strip() @@ -135,6 +136,8 @@ class PingRequest(BaseModel): @field_validator("ips") @classmethod def _ips(cls, v: list[str]) -> list[str]: + if len(v) > MAX_PING_IPS: + raise ValueError(f"Too many IPs: {len(v)} (max {MAX_PING_IPS})") for ip in v: try: ipaddress.ip_address(ip) diff --git a/backend/tests/test_validation.py b/backend/tests/test_validation.py index 27c99d5..1a88209 100644 --- a/backend/tests/test_validation.py +++ b/backend/tests/test_validation.py @@ -9,6 +9,7 @@ Couvre : """ import pytest from fastapi.testclient import TestClient +from passlib.context import CryptContext from sqlalchemy import text import sys, os @@ -17,6 +18,9 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from database import engine, Base from main import app, _migrate_users_must_change_password, _migrate_users_token_version, _migrate_users +_TEST_PASSWORD = "admin-test" +_pwd_ctx = CryptContext(schemes=["bcrypt"], deprecated="auto") + # --------------------------------------------------------------------------- # Fixtures @@ -29,6 +33,11 @@ def reset_db(): _migrate_users_must_change_password() _migrate_users_token_version() _migrate_users() + # Force a known password regardless of INITIAL_ADMIN_PASSWORD env var + with engine.connect() as conn: + hashed = _pwd_ctx.hash(_TEST_PASSWORD) + conn.execute(text(f"UPDATE users SET hashed_password=:h WHERE username='admin'"), {"h": hashed}) + conn.commit() yield Base.metadata.drop_all(bind=engine) @@ -55,7 +64,7 @@ def _get_token(client): with engine.connect() as conn: conn.execute(text("UPDATE users SET must_change_password=0 WHERE username='admin'")) conn.commit() - r = client.post("/api/auth/login", data={"username": "admin", "password": "admin"}) + r = client.post("/api/auth/login", data={"username": "admin", "password": _TEST_PASSWORD}) return r.json()["access_token"] @@ -189,6 +198,12 @@ class TestDiscoveryValidation: r = client.post("/api/discovery/ping", json={"ips": []}, headers=_auth(token)) assert r.status_code == 200 + def test_ping_too_many_ips_rejected(self, client): + token = _get_token(client) + ips = [f"10.0.{i // 256}.{i % 256}" for i in range(4097)] + r = client.post("/api/discovery/ping", json={"ips": ips}, headers=_auth(token)) + assert r.status_code == 422 + def test_scan_oversized_cidr_rejected(self, client): token = _get_token(client) r = client.post("/api/discovery/scan", json={