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 <noreply@anthropic.com>
This commit is contained in:
2026-05-18 18:16:08 +02:00
parent ec669c87b4
commit e8ca10f1b7
3 changed files with 20 additions and 2 deletions
+1 -1
View File
@@ -1,2 +1,2 @@
pytest>=7.4 pytest>=7.4
httpx>=0.25 httpx>=0.25,<0.28
+3
View File
@@ -16,6 +16,7 @@ router = APIRouter()
MAX_HOSTS_PER_TARGET = 1024 # refuse les /21 et plus larges MAX_HOSTS_PER_TARGET = 1024 # refuse les /21 et plus larges
MAX_HOSTS_TOTAL = 4096 # cap global sur l'ensemble des targets 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() _ENV_DNS = os.environ.get("DNS_SERVER", "").strip()
@@ -135,6 +136,8 @@ class PingRequest(BaseModel):
@field_validator("ips") @field_validator("ips")
@classmethod @classmethod
def _ips(cls, v: list[str]) -> list[str]: 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: for ip in v:
try: try:
ipaddress.ip_address(ip) ipaddress.ip_address(ip)
+16 -1
View File
@@ -9,6 +9,7 @@ Couvre :
""" """
import pytest import pytest
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from passlib.context import CryptContext
from sqlalchemy import text from sqlalchemy import text
import sys, os 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 database import engine, Base
from main import app, _migrate_users_must_change_password, _migrate_users_token_version, _migrate_users 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 # Fixtures
@@ -29,6 +33,11 @@ def reset_db():
_migrate_users_must_change_password() _migrate_users_must_change_password()
_migrate_users_token_version() _migrate_users_token_version()
_migrate_users() _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 yield
Base.metadata.drop_all(bind=engine) Base.metadata.drop_all(bind=engine)
@@ -55,7 +64,7 @@ def _get_token(client):
with engine.connect() as conn: with engine.connect() as conn:
conn.execute(text("UPDATE users SET must_change_password=0 WHERE username='admin'")) conn.execute(text("UPDATE users SET must_change_password=0 WHERE username='admin'"))
conn.commit() 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"] return r.json()["access_token"]
@@ -189,6 +198,12 @@ class TestDiscoveryValidation:
r = client.post("/api/discovery/ping", json={"ips": []}, headers=_auth(token)) r = client.post("/api/discovery/ping", json={"ips": []}, headers=_auth(token))
assert r.status_code == 200 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): def test_scan_oversized_cidr_rejected(self, client):
token = _get_token(client) token = _get_token(client)
r = client.post("/api/discovery/scan", json={ r = client.post("/api/discovery/scan", json={