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 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
@@ -10,6 +11,8 @@ import tempfile
|
|||||||
import pytest
|
import pytest
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from app.web.body_cap import BodyCapMiddleware
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def client(monkeypatch):
|
def client(monkeypatch):
|
||||||
@@ -62,3 +65,67 @@ def test_content_length_mare_body_mic_413_devreme(client):
|
|||||||
)
|
)
|
||||||
assert r.status_code == 413
|
assert r.status_code == 413
|
||||||
assert r.json()["cod"] == "CERERE_PREA_MARE"
|
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()
|
||||||
|
|||||||
@@ -45,11 +45,9 @@ def test_hsts_absent_pe_http(client):
|
|||||||
assert "Strict-Transport-Security" not in r.headers
|
assert "Strict-Transport-Security" not in r.headers
|
||||||
|
|
||||||
|
|
||||||
def test_hsts_prezent_pe_https():
|
def test_hsts_prezent_pe_https(monkeypatch):
|
||||||
tmp = tempfile.mkdtemp()
|
tmp = tempfile.mkdtemp()
|
||||||
import os as _os
|
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "sh_https.db"))
|
||||||
|
|
||||||
_os.environ["AUTOPASS_DB_PATH"] = _os.path.join(tmp, "sh_https.db")
|
|
||||||
from app.config import get_settings
|
from app.config import get_settings
|
||||||
get_settings.cache_clear()
|
get_settings.cache_clear()
|
||||||
from app.main import app
|
from app.main import app
|
||||||
@@ -61,4 +59,3 @@ def test_hsts_prezent_pe_https():
|
|||||||
"max-age=31536000; includeSubDomains"
|
"max-age=31536000; includeSubDomains"
|
||||||
)
|
)
|
||||||
get_settings.cache_clear()
|
get_settings.cache_clear()
|
||||||
_os.environ.pop("AUTOPASS_DB_PATH", None)
|
|
||||||
|
|||||||
@@ -32,6 +32,12 @@ RCLONE_REMOTE="${AUTOPASS_BACKUP_RCLONE_REMOTE:-}"
|
|||||||
log() { echo "[backup_db] $*"; }
|
log() { echo "[backup_db] $*"; }
|
||||||
err() { echo "[backup_db] EROARE: $*" >&2; }
|
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_PATHS=()
|
||||||
cleanup() {
|
cleanup() {
|
||||||
local p
|
local p
|
||||||
|
|||||||
Reference in New Issue
Block a user