Ataques a credenciais
Força bruta, credential stuffing, pulverização de senha, bloqueio de conta, atrasos progressivos e limite de taxa — com o que cada defesa quebra.
As três formas de atacar um login são diferentes, e a defesa que serve a uma não serve às outras. Confundi-las é como um sistema fica com bloqueio de conta e continua sendo invadido.
| Ataque | O que o atacante tem | A forma | O que o detecta |
|---|---|---|---|
| Força bruta | um usuário | muitas senhas nele | tentativas por conta |
| Credential stuffing | listas vazadas de usuário+senha | uma tentativa em cada conta | taxa por IP e por dispositivo |
| Pulverização | muitos usuários | uma senha comum em todos | taxa global de falhas |
Atraso progressivo#
A espera dobra a cada bloqueio, até um teto. Um limite fixo é contornado esperando o período; o crescimento torna a força bruta cara sem nunca trancar de vez quem só errou a senha.
adopt Arcane.Seguranca as Seg
// Depois de 3 falhas, espera 30s. Depois de mais 3, 60s. Depois,
// 120s — ate o teto. 'sucesso()' zera: sem isso, quem erra duas
// vezes por mes fica perto do bloqueio para sempre.
porta := Seg.tentativas(limite := 3, base := 30.0, teto := 3600.0)
cycle i from 1 to 2:
r := porta.falha("ana@loja.com")
out $" falha {i}: faltam {r['restantes']} tentativa(s)"
terceira := porta.falha("ana@loja.com")
assert terceira["bloqueado"]
out $" bloqueado por {terceira['segundos']}s"
// E o estado e POR CHAVE: outra conta nao e afetada.
assert porta.bloqueado("bruno@loja.com") is no
// O login certo zera a contagem.
porta.sucesso("ana@loja.com")
assert porta.bloqueado("ana@loja.com") is no
out "progressivo por conta, e nao um bloqueio duro"Limite de taxa#
O limitador usa balde de fichas: o balde enche continuamente, e não de uma vez por janela. Com janela fixa, um cliente gasta o limite no último segundo de uma e no primeiro da seguinte — o dobro do limite num instante, que é exatamente o que se queria evitar.
adopt Arcane.Seguranca as Seg
// 5 tentativas por minuto, por origem.
porta := Seg.limitador(5, periodo := 60.0)
cycle i from 1 to 5:
assert porta.permitir("203.0.113.7")
assert porta.permitir("203.0.113.7") is no
// 'espera' e o valor que vai no cabecalho 'Retry-After'.
out $" Retry-After: {porta.espera('203.0.113.7')}s"
// Outra origem nao e afetada — a chave e o que separa.
assert porta.permitir("198.51.100.4")A camada completa de um login#
adopt Arcane.Seguranca as Seg
adopt Arcane.Crypto as Crypto
steady POR_IP := Seg.limitador(20, periodo := 60.0)
steady POR_CONTA := Seg.tentativas(limite := 5, base := 30.0)
action entrar(email, senha, ip, guardada):
// 1. Taxa por origem — pega credential stuffing e pulverizacao.
given POR_IP.permitir(ip) is no:
yield {"ok": no, "motivo": "muitas tentativas", "espere": POR_IP.espera(ip)}
// 2. Atraso progressivo por conta — pega forca bruta.
given POR_CONTA.bloqueado(email):
yield {"ok": no, "motivo": "conta temporariamente bloqueada",
"espere": POR_CONTA.falta(email)}
// 3. A conferencia em si, em tempo constante por dentro.
given Crypto.verify_password(senha, guardada) is no:
POR_CONTA.falha(email)
// A MESMA mensagem para usuario inexistente e senha errada:
// distingui-las entrega quais contas existem.
yield {"ok": no, "motivo": "usuario ou senha invalidos"}
POR_CONTA.sucesso(email)
yield {"ok": yes}
guardada := Crypto.hash_password("uma senha bem forte 2026")
assert entrar("ana@loja.com", "errada", "203.0.113.7", guardada)["ok"] is no
assert entrar("ana@loja.com", "uma senha bem forte 2026", "203.0.113.7", guardada)["ok"]
out "taxa por origem, atraso por conta, mensagem unica"Enumeração de usuários#
Se “usuário não existe” e “senha incorreta” são respostas diferentes, o atacante descobre quais contas existem antes de tentar qualquer senha — e passa a usar as listas vazadas só onde elas valem.
| Onde vaza | Como fechar |
|---|---|
| mensagem de erro do login | a mesma frase para os dois casos |
| tempo de resposta | derive a senha mesmo quando o usuário não existe |
| cadastro (“e-mail já usado”) | responda sempre igual e informe por e-mail |
| redefinição de senha | “se houver conta, enviamos um link” |
| código HTTP diferente | o mesmo 401 nos dois casos |
Outras defesas, e o veredito#
| Defesa | Estado aqui | Observação |
|---|---|---|
| senha vazada recusada no cadastro | existe — Seg.vazada | k-anonimato; a senha não sai da máquina |
| CAPTCHA | não existe | é serviço de terceiro; ponha-o depois de N falhas, não antes |
| MFA | existe — TOTP | a defesa que sobrevive à senha vazada |
| impressão digital de dispositivo | não existe | tem implicação de privacidade; avalie antes |
| alerta de login novo | seu e-mail + Seg.auditoria | detecção, e não prevenção |
| bloqueio por geografia | não existe | alta taxa de falso positivo |