From d7a08ddd8d2035eacd515a6c629435e7a37676d1 Mon Sep 17 00:00:00 2001 From: Gustavo Date: Sat, 15 Aug 2026 11:17:41 -0300 Subject: [PATCH] security: rate limit no login, cookies Secure condicional, security headers e deps com CVE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- Dockerfile | 7 +++++ app/main.py | 41 ++++++++++++++++++++++++++- app/rate_limit.py | 52 +++++++++++++++++++++++++++++++++++ app/routers/auth.py | 40 +++++++++++++++++++++++++-- requirements.txt | 18 ++++++++++-- tests/test_rate_limit.py | 58 +++++++++++++++++++++++++++++++++++++++ tests/test_router_auth.py | 23 +++++++++++++++- 7 files changed, 232 insertions(+), 7 deletions(-) create mode 100644 app/rate_limit.py create mode 100644 tests/test_rate_limit.py diff --git a/Dockerfile b/Dockerfile index df5efa5..d3d1c4f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,5 +3,12 @@ WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY app ./app +# Não roda como root dentro do container — reduz o estrago de uma +# eventual RCE (mesmo sem vulnerabilidade conhecida hoje, é uma +# mitigação de baixo custo e sem efeito colateral aqui, já que a app não +# precisa gravar nada no filesystem além de escutar a porta 8000, que não +# é privilegiada). +RUN useradd --no-create-home --shell /usr/sbin/nologin painel +USER painel EXPOSE 8000 CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/app/main.py b/app/main.py index 81dce00..67c397e 100644 --- a/app/main.py +++ b/app/main.py @@ -45,11 +45,50 @@ async def csrf_cookie_middleware(request: Request, call_next): if precisa_definir_cookie: response.set_cookie( - CSRF_COOKIE_NAME, request.state.csrf_token, httponly=True, samesite="strict" + CSRF_COOKIE_NAME, request.state.csrf_token, httponly=True, samesite="strict", + # Secure só quando a requisição já chegou em HTTPS — ver o + # comentário equivalente em app/routers/auth.py sobre o + # cookie de sessão (mesma razão, mesmo cuidado com proxy). + secure=request.url.scheme == "https", ) return response +@app.middleware("http") +async def security_headers_middleware(request: Request, call_next): + """ + Cabeçalhos de resposta que não dependem de nenhuma lógica de negócio, + só reduzem a superfície de ataque do navegador: impedem o painel de + ser carregado dentro de um