git-subtree-dir: gerenciador git-subtree-mainline:f4f546c928git-subtree-split:34cc9c75a5
53 lines
2.2 KiB
Python
53 lines
2.2 KiB
Python
"""
|
|
Limitador de tentativas de login por IP de origem — proteção simples
|
|
contra força bruta de senha em POST /login (app/routers/auth.py), que é
|
|
uma rota pré-autenticação e portanto alcançável por qualquer visitante.
|
|
|
|
Em memória (dict por processo), no mesmo espírito de app/session.py (chave
|
|
de assinatura em memória) e app/db/connection.py (sem pool): este painel
|
|
roda como container único, sem múltiplas réplicas atrás de um load
|
|
balancer, então não precisa de armazenamento compartilhado (Redis etc.)
|
|
para isso funcionar. Reinício do processo zera o histórico — efeito
|
|
colateral aceito, mesma lógica do SESSION_SECRET em memória.
|
|
|
|
Se um dia o painel passar a rodar atrás de um proxy reverso, a origem
|
|
(hoje request.client.host, lida em app/routers/auth.py) precisa passar a
|
|
vir de X-Forwarded-For validado contra uma lista de proxies confiáveis —
|
|
sem isso, toda tentativa apareceria vindo do IP do proxy e o limite
|
|
passaria a valer para todo mundo junto (um IP externo malicioso poderia
|
|
bloquear logins de todo mundo).
|
|
"""
|
|
import time
|
|
|
|
MAX_TENTATIVAS = 5
|
|
JANELA_SEGUNDOS = 5 * 60 # 5 minutos
|
|
|
|
_falhas: dict[str, list[float]] = {}
|
|
|
|
|
|
def _recentes(origem: str, momento: float) -> list[float]:
|
|
limite = momento - JANELA_SEGUNDOS
|
|
return [t for t in _falhas.get(origem, []) if t > limite]
|
|
|
|
|
|
def registrar_falha(origem: str, agora: float | None = None) -> None:
|
|
"""Chamado a cada tentativa de login com credencial inválida."""
|
|
momento = agora if agora is not None else time.time()
|
|
recentes = _recentes(origem, momento)
|
|
recentes.append(momento)
|
|
_falhas[origem] = recentes
|
|
|
|
|
|
def bloqueado(origem: str, agora: float | None = None) -> bool:
|
|
"""True quando essa origem já teve MAX_TENTATIVAS falhas nos últimos
|
|
JANELA_SEGUNDOS — POST /login deve recusar antes mesmo de checar a
|
|
credencial enviada."""
|
|
momento = agora if agora is not None else time.time()
|
|
return len(_recentes(origem, momento)) >= MAX_TENTATIVAS
|
|
|
|
|
|
def limpar(origem: str) -> None:
|
|
"""Chamado após um login bem-sucedido — não faz sentido continuar
|
|
contando falhas antigas contra quem acabou de provar a senha certa."""
|
|
_falhas.pop(origem, None)
|