Pular para o conteúdo

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.

AtaqueO que o atacante temA formaO que o detecta
Força brutaum usuáriomuitas senhas neletentativas por conta
Credential stuffinglistas vazadas de usuário+senhauma tentativa em cada contataxa por IP e por dispositivo
Pulverizaçãomuitos usuáriosuma senha comum em todostaxa 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.

dataforge
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.

dataforge
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#

dataforge
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 vazaComo fechar
mensagem de erro do logina mesma frase para os dois casos
tempo de respostaderive 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 diferenteo mesmo 401 nos dois casos

Outras defesas, e o veredito#

DefesaEstado aquiObservação
senha vazada recusada no cadastroexisteSeg.vazadak-anonimato; a senha não sai da máquina
CAPTCHAnão existeé serviço de terceiro; ponha-o depois de N falhas, não antes
MFAexiste — TOTPa defesa que sobrevive à senha vazada
impressão digital de dispositivonão existetem implicação de privacidade; avalie antes
alerta de login novoseu e-mail + Seg.auditoriadetecção, e não prevenção
bloqueio por geografianão existealta taxa de falso positivo