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:
@@ -1,2 +1,2 @@
|
|||||||
pytest>=7.4
|
pytest>=7.4
|
||||||
httpx>=0.25
|
httpx>=0.25,<0.28
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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={
|
||||||
|
|||||||
Reference in New Issue
Block a user