Findings security-review P1/P2 (2026-07-03): - headere de securitate pe toate raspunsurile (nosniff, X-Frame-Options, Referrer-Policy, HSTS doar pe HTTPS) + teste - body-cap global 10MB ca middleware ASGI pur (413 inainte de parserul multipart/JSON; verificarea per-endpoint ramane strat 2) - imagine Docker non-root (uid 10001), port 8010 aliniat, loguri pe volumul /data - fail-fast la boot cu rar_env=prod fara AUTOPASS_REQUIRE_API_KEY sau AUTOPASS_SESSION_SECRET - compose: env-uri critice obligatorii (:?) ca api/worker sa nu diverga tacit; FORWARDED_ALLOW_IPS ca rate-limit-ul sa vada IP-ul real dupa Traefik - signup fara PII in stdout: log_event in loc de print cu email (idem notify degradat) - ratelimit: sterge cheile fara timestamp-uri valide (crestere monotona a memoriei pe IP-uri reale) - backup criptat SQLite (backup online API, gpg AES256) + verificare restore + docs/backup.md Suita completa verde: 1557 passed, 1 skipped (live). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
78 lines
2.8 KiB
Python
78 lines
2.8 KiB
Python
"""Teste F5: `_hits` nu creste monoton pe viata procesului.
|
|
|
|
Cheile ale caror timestamp-uri au expirat complet trebuie sterse din `_hits`
|
|
(nu doar golite), altfel dictionarul acumuleaza o intrare per IP vazut vreodata.
|
|
Semantica ferestrei glisante (permite/blocheaza) trebuie sa ramana neschimbata.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from app.web import ratelimit
|
|
from app.web.ratelimit import check_rate_limit
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _clean_hits():
|
|
ratelimit._hits.clear()
|
|
yield
|
|
ratelimit._hits.clear()
|
|
|
|
|
|
def test_cheie_blocata_fara_timestamp_uri_valide_e_stearsa(monkeypatch):
|
|
"""max_hits=0 -> orice cerere e blocata; daca fereastra a expirat complet
|
|
(filtrarea produce o lista goala), cheia nu trebuie sa ramana in `_hits`."""
|
|
now = [1000.0]
|
|
monkeypatch.setattr(ratelimit.time, "monotonic", lambda: now[0])
|
|
|
|
key = "ip_blocat_gol"
|
|
assert check_rate_limit(key, max_hits=0, window_s=60) is False
|
|
# Nimic adaugat (max_hits=0 blocheaza tot) si lista era goala -> cheia disparuta.
|
|
assert key not in ratelimit._hits
|
|
|
|
|
|
def test_cheie_activa_ramane_dupa_permis(monkeypatch):
|
|
"""O cerere permisa isi adauga timestamp-ul -> cheia activa ramane in `_hits`."""
|
|
now = [1000.0]
|
|
monkeypatch.setattr(ratelimit.time, "monotonic", lambda: now[0])
|
|
|
|
key = "ip_activ"
|
|
assert check_rate_limit(key, max_hits=2, window_s=60) is True
|
|
assert key in ratelimit._hits
|
|
assert ratelimit._hits[key] == [1000.0]
|
|
|
|
|
|
def test_semantica_max_hits_si_fereastra_neschimbata(monkeypatch):
|
|
"""max_hits atinse -> False; dupa expirarea ferestrei -> True din nou."""
|
|
now = [1000.0]
|
|
monkeypatch.setattr(ratelimit.time, "monotonic", lambda: now[0])
|
|
|
|
key = "ip_fereastra"
|
|
assert check_rate_limit(key, max_hits=2, window_s=10) is True
|
|
assert check_rate_limit(key, max_hits=2, window_s=10) is True
|
|
# A treia cerere in aceeasi fereastra -> blocata.
|
|
assert check_rate_limit(key, max_hits=2, window_s=10) is False
|
|
|
|
# Trece timpul peste fereastra -> toate timestamp-urile expira.
|
|
now[0] += 11
|
|
assert check_rate_limit(key, max_hits=2, window_s=10) is True
|
|
# Doar noua cerere ramane in lista (cele vechi, expirate, au fost curatate).
|
|
assert ratelimit._hits[key] == [1011.0]
|
|
|
|
|
|
def test_cheie_dispare_dupa_expirare_completa_si_reblocare(monkeypatch):
|
|
"""O cheie blocata, ramasa fara timestamp-uri valide dupa expirarea ferestrei
|
|
(fara sa mai apara alte cereri intre timp), nu trebuie sa ramana in `_hits`
|
|
la urmatorul apel care o gaseste goala si tot o blocheaza (max_hits=0)."""
|
|
now = [1000.0]
|
|
monkeypatch.setattr(ratelimit.time, "monotonic", lambda: now[0])
|
|
|
|
key = "ip_reblocat"
|
|
check_rate_limit(key, max_hits=0, window_s=5)
|
|
assert key not in ratelimit._hits
|
|
|
|
now[0] += 100
|
|
assert check_rate_limit(key, max_hits=0, window_s=5) is False
|
|
assert key not in ratelimit._hits
|