security: rate limit no login, cookies Secure condicional, security headers e deps com CVE
- app/rate_limit.py: bloqueia por IP após 5 tentativas de senha erradas em 5 minutos em POST /login (rota pré-autenticação, hoje sem nenhuma proteção contra força bruta). - Cookie de sessão (routers/auth.py) e cookie CSRF (main.py) passam a sair com Secure quando a requisição chega em HTTPS (request.url.scheme) — hoje é sempre False porque produção ainda serve HTTP puro, mas o cookie passa a ficar protegido automaticamente assim que um TLS for colocado na frente, sem precisar editar isso de novo. - main.py: novo middleware de security headers (X-Content-Type-Options, X-Frame-Options: DENY, Referrer-Policy, Content-Security-Policy, Strict-Transport-Security condicional a HTTPS). - requirements.txt: fastapi 0.115.0 -> 0.115.6 (starlette >=0.40.0, corrige DoS por memória não limitada ao parsear multipart/form-data — GHSA-f96h-pmfr-66vw), python-multipart 0.0.9 -> 0.0.20 (várias vulnerabilidades de DoS corrigidas), jinja2 3.1.4 -> 3.1.6 (escapes de sandbox). httpx/uvicorn/PyMySQL já estavam em versões sem CVE conhecida — conferido via osv.dev. - Dockerfile: container não roda mais como root. Suíte completa (181 testes) passando, incluindo testes novos para o rate limiter (unitários + integração via POST /login) e smoke test manual confirmando headers, fluxo de login com CSRF e o bloqueio por força bruta. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
b0830651f4
commit
d7a08ddd8d
@@ -0,0 +1,58 @@
|
||||
from app import rate_limit
|
||||
|
||||
|
||||
def _origem_unica() -> str:
|
||||
"""Cada teste usa uma chave própria — evita que o dict em memória do
|
||||
módulo (compartilhado entre testes) vaze estado de um teste pro outro."""
|
||||
import uuid
|
||||
|
||||
return f"teste-{uuid.uuid4()}"
|
||||
|
||||
|
||||
def test_nao_bloqueado_sem_nenhuma_falha():
|
||||
assert rate_limit.bloqueado(_origem_unica()) is False
|
||||
|
||||
|
||||
def test_nao_bloqueado_abaixo_do_limite():
|
||||
origem = _origem_unica()
|
||||
agora = 1_000_000.0
|
||||
for _ in range(rate_limit.MAX_TENTATIVAS - 1):
|
||||
rate_limit.registrar_falha(origem, agora=agora)
|
||||
assert rate_limit.bloqueado(origem, agora=agora) is False
|
||||
|
||||
|
||||
def test_bloqueado_ao_atingir_o_limite():
|
||||
origem = _origem_unica()
|
||||
agora = 1_000_000.0
|
||||
for _ in range(rate_limit.MAX_TENTATIVAS):
|
||||
rate_limit.registrar_falha(origem, agora=agora)
|
||||
assert rate_limit.bloqueado(origem, agora=agora) is True
|
||||
|
||||
|
||||
def test_falhas_fora_da_janela_nao_contam():
|
||||
origem = _origem_unica()
|
||||
agora = 1_000_000.0
|
||||
fora_da_janela = agora - rate_limit.JANELA_SEGUNDOS - 1
|
||||
for _ in range(rate_limit.MAX_TENTATIVAS):
|
||||
rate_limit.registrar_falha(origem, agora=fora_da_janela)
|
||||
assert rate_limit.bloqueado(origem, agora=agora) is False
|
||||
|
||||
|
||||
def test_limpar_reseta_o_historico():
|
||||
origem = _origem_unica()
|
||||
agora = 1_000_000.0
|
||||
for _ in range(rate_limit.MAX_TENTATIVAS):
|
||||
rate_limit.registrar_falha(origem, agora=agora)
|
||||
assert rate_limit.bloqueado(origem, agora=agora) is True
|
||||
|
||||
rate_limit.limpar(origem)
|
||||
assert rate_limit.bloqueado(origem, agora=agora) is False
|
||||
|
||||
|
||||
def test_origens_diferentes_nao_se_afetam():
|
||||
origem_a, origem_b = _origem_unica(), _origem_unica()
|
||||
agora = 1_000_000.0
|
||||
for _ in range(rate_limit.MAX_TENTATIVAS):
|
||||
rate_limit.registrar_falha(origem_a, agora=agora)
|
||||
assert rate_limit.bloqueado(origem_a, agora=agora) is True
|
||||
assert rate_limit.bloqueado(origem_b, agora=agora) is False
|
||||
Reference in New Issue
Block a user