From 164f90d6036d83b35bd10486ed23ef3fa90ef9fb Mon Sep 17 00:00:00 2001 From: Claude Agent Date: Mon, 6 Jul 2026 13:01:00 +0000 Subject: [PATCH] fix(securitate): inchide nit-urile din review-ul hardening (N1-N3) - N1: test HSTS pe monkeypatch.setenv (fara env var scursa la assert picat) - N2: backup_db.sh refuza AUTOPASS_BACKUP_KEEP < 1 (retentia ar fi sters backup-ul abia creat) - N3: teste ASGI directe pentru Content-Length malformat/negativ in BodyCapMiddleware (comportamentul defensiv exista deja, acum e fixat in teste) Suita completa: 1559 passed, 1 skipped. Co-Authored-By: Claude Fable 5 --- tests/test_body_cap.py | 67 ++++++++++++++++++++++++++++++++++ tests/test_security_headers.py | 7 +--- tools/backup_db.sh | 6 +++ 3 files changed, 75 insertions(+), 5 deletions(-) diff --git a/tests/test_body_cap.py b/tests/test_body_cap.py index 6106c1e..1bcba3c 100644 --- a/tests/test_body_cap.py +++ b/tests/test_body_cap.py @@ -2,6 +2,7 @@ from __future__ import annotations +import asyncio import io import json import os @@ -10,6 +11,8 @@ import tempfile import pytest from fastapi.testclient import TestClient +from app.web.body_cap import BodyCapMiddleware + @pytest.fixture() def client(monkeypatch): @@ -62,3 +65,67 @@ def test_content_length_mare_body_mic_413_devreme(client): ) assert r.status_code == 413 assert r.json()["cod"] == "CERERE_PREA_MARE" + + +async def _dummy_app(scope, receive, send): + """App ASGI minimal: consuma tot body-ul (ca un endpoint real), apoi 200.""" + more_body = True + while more_body: + message = await receive() + more_body = message.get("more_body", False) + await send({"type": "http.response.start", "status": 200, "headers": []}) + await send({"type": "http.response.body", "body": b"ok"}) + + +def _run_body_cap(headers, body_chunks): + """Ruleaza BodyCapMiddleware direct pe un scope/receive/send ASGI construit manual + (TestClient/httpx nu lasa Content-Length malformat/negativ sa treaca la request real).""" + scope = {"type": "http", "method": "POST", "path": "/x", "headers": headers} + chunks = list(body_chunks) + + async def receive(): + if not chunks: + return {"type": "http.request", "body": b"", "more_body": False} + body, more = chunks.pop(0) + return {"type": "http.request", "body": body, "more_body": more} + + messages = [] + + async def send(message): + messages.append(message) + + async def run(): + await BodyCapMiddleware(_dummy_app)(scope, receive, send) + + asyncio.run(run()) + return next(m["status"] for m in messages if m["type"] == "http.response.start") + + +def test_content_length_malformat_nu_crapa(monkeypatch): + monkeypatch.setenv("AUTOPASS_MAX_REQUEST_BYTES", "1000") + from app.config import get_settings + + get_settings.cache_clear() + try: + status = _run_body_cap( + headers=[(b"content-length", b"abc")], + body_chunks=[(b"body mic", False)], + ) + assert status == 200 + finally: + get_settings.cache_clear() + + +def test_content_length_negativ_nu_crapa_dar_plafonul_ramane_aplicat(monkeypatch): + monkeypatch.setenv("AUTOPASS_MAX_REQUEST_BYTES", "1000") + from app.config import get_settings + + get_settings.cache_clear() + try: + status = _run_body_cap( + headers=[(b"content-length", b"-5")], + body_chunks=[(b"x" * 2000, False)], + ) + assert status == 413 + finally: + get_settings.cache_clear() diff --git a/tests/test_security_headers.py b/tests/test_security_headers.py index 18baa3c..eec2f3f 100644 --- a/tests/test_security_headers.py +++ b/tests/test_security_headers.py @@ -45,11 +45,9 @@ def test_hsts_absent_pe_http(client): assert "Strict-Transport-Security" not in r.headers -def test_hsts_prezent_pe_https(): +def test_hsts_prezent_pe_https(monkeypatch): tmp = tempfile.mkdtemp() - import os as _os - - _os.environ["AUTOPASS_DB_PATH"] = _os.path.join(tmp, "sh_https.db") + monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "sh_https.db")) from app.config import get_settings get_settings.cache_clear() from app.main import app @@ -61,4 +59,3 @@ def test_hsts_prezent_pe_https(): "max-age=31536000; includeSubDomains" ) get_settings.cache_clear() - _os.environ.pop("AUTOPASS_DB_PATH", None) diff --git a/tools/backup_db.sh b/tools/backup_db.sh index 2fba36e..6777b80 100755 --- a/tools/backup_db.sh +++ b/tools/backup_db.sh @@ -32,6 +32,12 @@ RCLONE_REMOTE="${AUTOPASS_BACKUP_RCLONE_REMOTE:-}" log() { echo "[backup_db] $*"; } err() { echo "[backup_db] EROARE: $*" >&2; } +if ! [[ "$KEEP" =~ ^[0-9]+$ ]] || [[ "$KEEP" -lt 1 ]]; then + err "AUTOPASS_BACKUP_KEEP trebuie sa fie un numar intreg >= 1 (primit: '$KEEP')." + err "cu KEEP=0 retentia ar sterge inclusiv backup-ul abia creat." + exit 1 +fi + CLEANUP_PATHS=() cleanup() { local p