feat: protecao CSRF (double-submit cookie) e paginacao da lista de clientes
CSRF: novo app/csrf.py + middleware em app/main.py garantem um cookie csrf_token por navegador; todo formulario que muda estado (criar/editar/ excluir cliente, adicionar/editar/excluir contato) embute esse valor como campo oculto, e Depends(exigir_csrf) confere no POST que o valor enviado bate com o cookie antes de deixar a rota executar. Verificado ponta a ponta com o app real rodando (curl): token errado -> 403, token correto -> 303. Paginacao: db_clientes.listar_clientes ganha offset, nova contar_clientes(); GET /clientes aceita ?pagina=N (100 clientes por pagina), template mostra Anterior/Proxima e o total. 53/53 testes passando (8 novos).
This commit is contained in:
+44
@@ -0,0 +1,44 @@
|
||||
"""
|
||||
Proteção CSRF via "double-submit cookie": o middleware registrado em
|
||||
app/main.py garante que todo navegador que acessa o painel tenha um cookie
|
||||
"csrf_token" (gerado uma vez, aleatório, HttpOnly). Todo formulário que
|
||||
muda estado (POST) embute esse mesmo valor como campo oculto — os
|
||||
templates leem de request.state.csrf_token, que o middleware preenche a
|
||||
cada requisição. Toda rota POST que muda estado usa Depends(exigir_csrf),
|
||||
que confere se o valor enviado no formulário bate com o valor do cookie.
|
||||
|
||||
Por quê isso barra CSRF: um site hostil consegue forçar o navegador da
|
||||
vítima a mandar um POST para o painel (o cookie vai junto automaticamente),
|
||||
mas não consegue LER o valor do cookie para replicá-lo como campo oculto do
|
||||
formulário forjado — só JavaScript rodando na própria origem do painel
|
||||
consegue (e não roda em outra origem, por causa do same-origin policy).
|
||||
"""
|
||||
import secrets
|
||||
|
||||
from fastapi import HTTPException, Request, status
|
||||
|
||||
CSRF_COOKIE_NAME = "csrf_token"
|
||||
|
||||
|
||||
def gerar_token() -> str:
|
||||
return secrets.token_urlsafe(32)
|
||||
|
||||
|
||||
def token_do_contexto(request: Request) -> str:
|
||||
"""
|
||||
Lê o token que o middleware colocou em request.state.csrf_token.
|
||||
Retorna "" se o middleware não estiver montado (por exemplo, testes que
|
||||
sobem só um router isolado, sem app/main.py) — nesse caso o formulário
|
||||
simplesmente não terá csrf_token preenchido, e exigir_csrf abaixo vai
|
||||
rejeitar o POST a menos que o teste monte o cookie/campo manualmente.
|
||||
"""
|
||||
return getattr(request.state, "csrf_token", "")
|
||||
|
||||
|
||||
async def exigir_csrf(request: Request) -> None:
|
||||
"""Dependency para toda rota POST que muda estado."""
|
||||
form = await request.form()
|
||||
token_formulario = str(form.get(CSRF_COOKIE_NAME, ""))
|
||||
token_cookie = request.cookies.get(CSRF_COOKIE_NAME, "")
|
||||
if not token_cookie or not token_formulario or not secrets.compare_digest(token_formulario, token_cookie):
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Token CSRF ausente ou inválido.")
|
||||
Reference in New Issue
Block a user