Enumeração de usuários é a capacidade de um atacante descobrir quais e-mails ou nomes de usuário existem em uma aplicação, observando como ela responde. Sozinha, ela raramente é “crítica”. O valor dela está no que vem depois: credential stuffing direcionado, password spraying, phishing personalizado e força bruta com metade do trabalho já feito. Saber quem tem conta reduz drasticamente o espaço de busca do atacante.
Quase sempre o problema começa numa boa intenção de UX. A aplicação tenta ser prestativa, devolve uma mensagem de erro “útil” e, sem querer, monta um oráculo.
O oráculo no formulário de login
Considere duas respostas de login:
# Usuário não existe
"Não encontramos uma conta com esse e-mail."
# Senha incorreta
"Senha incorreta. Tente novamente."
Para o usuário legítimo, parece atencioso. Para o atacante, é um oráculo: ele envia uma senha qualquer e lê a resposta. “Conta não encontrada” significa e-mail inexistente; “Senha incorreta” significa e-mail válido. Com uma lista de e-mails vazados, ele filtra em minutos exatamente quais têm conta no seu sistema.
Para fechar isso, devolva uma mensagem genérica e idêntica nos dois casos:
"E-mail ou senha inválidos."
O oráculo escondido no reset de senha
O fluxo de “esqueci minha senha” costuma ser o mais negligenciado. É comum ver algo assim:
# E-mail cadastrado
"Enviamos um link de redefinição para o seu e-mail."
# E-mail não cadastrado
"Esse e-mail não está cadastrado."
De novo, a segunda mensagem confirma que a conta existe. A resposta certa é sempre a mesma, exista ou não a conta:
"Se houver uma conta com esse e-mail, enviaremos um link de redefinição."
O detalhe que importa: o e-mail só é disparado de fato quando a conta existe, mas a resposta HTTP é idêntica nos dois casos.
O oráculo no cadastro
O registro tem uma tensão real. Você precisa avisar que “esse e-mail já está em uso”, e esse aviso, por definição, é enumeração. Dá para resolver sem vazar de duas formas:
- Confirmação por e-mail antes de revelar qualquer coisa. O formulário sempre responde “enviamos um e-mail para concluir o cadastro”. Se o endereço já tem conta, o e-mail recebido diz “você já tem uma conta” e oferece o reset. Quem não controla a caixa do alvo não aprende nada.
- Tratar a verificação de disponibilidade com os mesmos cuidados de rate limiting e CAPTCHA, ciente do trade-off de UX.
Canais que vazam além do texto
Mesmo com mensagens uniformes, a aplicação ainda pode vazar por canais laterais:
Diferença de tempo de resposta
Se a aplicação só executa o cálculo caro de verificação de hash de senha quando o usuário existe, a resposta para um usuário válido demora mensuravelmente mais que para um inexistente. O atacante mede a latência e enumera sem ler uma única mensagem.
# VULNERÁVEL: só faz o trabalho caro quando o usuário existe
def login(email, senha):
user = db.find_user(email)
if not user:
return "E-mail ou senha inválidos." # responde rápido
if not verify_password(senha, user.hash): # custoso (bcrypt/argon2)
return "E-mail ou senha inválidos." # responde devagar
return start_session(user)
A defesa é gastar o mesmo tempo nos dois caminhos, verificando o hash contra um valor dummy quando o usuário não existe:
DUMMY_HASH = hash_password("senha-impossivel-de-acertar")
def login(email, senha):
user = db.find_user(email)
hash_alvo = user.hash if user else DUMMY_HASH
senha_ok = verify_password(senha, hash_alvo) # mesmo custo sempre
if user and senha_ok:
return start_session(user)
return "E-mail ou senha inválidos."
Diferença de comportamento de bloqueio
Se a conta é bloqueada após N tentativas somente quando existe, a presença ou ausência do bloqueio revela contas válidas. O comportamento de lockout e os contadores precisam ser consistentes nos dois casos.
Outros sinais
Códigos de status HTTP distintos, redirecionamentos diferentes, presença ou ausência de um cookie, tamanho do corpo da resposta: qualquer diferença observável entre “existe” e “não existe” é um vetor. A regra que cobre todos eles é a mesma, a resposta tem de ser indistinguível.
A tensão com a usabilidade
Mensagens genéricas frustram um pouco o usuário legítimo, e times de produto costumam resistir. O equilíbrio que funciona na prática:
- Mensagem genérica na resposta imediata do formulário, que é o canal que o atacante observa em escala.
- Orientação detalhada por um canal autenticado, o e-mail enviado para o endereço informado, que só o dono da caixa lê.
- Rate limiting e CAPTCHA para que, mesmo com algum resíduo de diferença, a enumeração em massa seja inviável.
Como testamos enumeração de usuários
- Comparar respostas de login para um e-mail claramente inexistente e um e-mail conhecido — texto, status, tempo, cookies, redirects.
- Repetir no reset de senha e no cadastro.
- Medir a latência em lote para detectar oráculo por timing mesmo quando o texto é uniforme.
- Testar os limites de rate limiting/CAPTCHA: dá para automatizar milhares de checagens?
- Avaliar o impacto encadeado com listas de e-mails vazadas (credential stuffing direcionado).
Checklist de mitigação
- Mensagem idêntica e genérica em login, reset e cadastro (“e-mail ou senha inválidos” / “se houver conta, enviaremos o link”).
- Tempo de resposta constante: verificar hash contra dummy quando o usuário não existe.
- Mesmos status HTTP, redirects, cookies e tamanho de corpo nos dois casos.
- Comportamento de lockout/contadores consistente, independente de a conta existir.
- Rate limiting por IP/identificador e CAPTCHA adaptativo nos fluxos de auth.
- Revelar “e-mail já cadastrado” apenas por canal autenticado (o próprio e-mail).
- Monitorar e alertar picos de falhas de login/reset (sinal de enumeração em curso).
Enumeração de usuários mora na diferença entre duas respostas, e por isso é uma falha de design antes de ser de código. Eliminá-la dá pouco trabalho técnico e muito trabalho de disciplina: garantir que a aplicação diga exatamente a mesma coisa, no mesmo tempo, exista a conta ou não.


