Incidente sério de verdade quase nunca começa com alarme tocando. Começa quieto.
Numa tarde comum, um punhado de contas foi bloqueado quase ao mesmo tempo. Poucas, mas nenhuma delas aleatória. Era o tipo de conta que alguém escolhe a dedo: gente próxima do centro da operação, o alvo de quem fez o dever de casa antes de bater na porta. No dia anterior, essas mesmas contas tinham recebido tentativas de trocar o código de verificação. Reconhecimento. O ataque em si veio depois, e foi rápido: da primeira conta comprometida à última, quarenta minutos.
Quando nos chamaram, a pergunta era a de sempre, e a mais difícil de responder no calor do momento: isso é um bug, ou é alguém?
Rastrear primeiro, consertar depois#
A tentação, num incidente, é sair mexendo. A gente resistiu.
O primeiro passo foi ler os registros de auditoria do provedor de identidade: quem trocou a senha de quem, e quando. Ali a história parou de ser hipótese. Cada troca de senha administrativa batia, ao segundo, com o horário de bloqueio de uma conta. Não era um usuário confuso clicando em "esqueci a senha". Era uma mão externa, metódica, passando de conta em conta pelo mesmo caminho.
Saber como mudou tudo. Sem isso, a gente estaria tapando buracos no escuro.
A porta que estava trancada por fora#
A causa raiz é o tipo de coisa que dá um frio na espinha justamente por ser tão banal.
Meses antes, numa migração, ficou para trás um "modo de compatibilidade" no fluxo de recuperação de senha. A ideia era gentil: não quebrar clientes antigos enquanto a base migrava. O efeito colateral não era gentil nenhum. Nesse caminho legado, dava para concluir a redefinição de senha e receber uma sessão válida conhecendo apenas um identificador público da vítima: um número que, convenhamos, não é segredo de ninguém.
Um dado que qualquer um descobre não é uma senha. Nunca foi. Mas o caminho legado tratava como se fosse.
O detalhe que mais incomoda: a flag que ligava esse comportamento já vinha desligada. No papel, o risco não existia. Só que "desligado por padrão de configuração" e "não existe no código" são coisas muito diferentes. A porta estava trancada, mas a chave velha continuava pendurada no prego ao lado, e o código sabia usá-la.
A parte que ninguém gosta de admitir#
A gente queria devolver ao cliente o endereço de onde o ataque partiu. Não deu.
Para aquela janela, o IP de origem era irrecuperável. A aplicação ficava atrás de uma CDN, e a telemetria da época simplesmente não guardava o IP real de quem chamava a API. Os logs de borda que teriam esse dado já haviam expirado quando começamos a investigar.
Sem telemetria de origem, não existe atribuição.
Essa foi a lição mais desconfortável do incidente, e a gente não vai fingir que foi de outro jeito. Você contém o ataque, entende o mecanismo, fecha a brecha, e ainda assim não consegue apontar para o mapa. Não por incompetência de ninguém: por um campo de log que faltava. Foi a primeira coisa a entrar na fila de correção.
Tapar o buraco não é o trabalho. Blindar o cômodo é#
Aqui está a distinção que, para a gente, separa uma resposta a incidente boa de uma medíocre.
A resposta medíocre desliga a flag, respira aliviada e vai embora. O buraco está tapado, afinal. Só que o atacante já demonstrou o caminho, e o caminho é mais largo do que aquele único buraco.
Então, depois de estancar a contenção no mesmo dia, a gente tratou o fluxo inteiro como suspeito:
- A sessão morre com a senha. Ao redefinir a senha, todas as sessões e tokens antigos daquele usuário são revogados na hora. Um token roubado não sobrevive à troca. Antes, sobrevivia.
- O caminho de recuperação ficou estrito. Saiu o endpoint público que deixava qualquer um confirmar se uma conta existia. Entrou confirmação real de posse antes de qualquer reset, e limite de tentativas no login para conter força bruta.
- A gente passou a enxergar. IP real do chamador nos logs, aviso por e-mail ao usuário a cada troca de senha, e alarmes que disparam quando o padrão do próprio incidente, muitas redefinições em pouco tempo, reaparece. Da próxima vez o alerta chega antes do bloqueio, não depois.
- Barreiras na borda. Rate limit e bloqueio de tráfego de datacenter nas rotas de recuperação, aplicados onde o IP é confiável de verdade: na CDN, antes de a requisição sequer tocar a aplicação.
Nenhuma dessas mudanças, sozinha, teria impedido o ataque. Juntas, elas transformam o mesmo movimento: de invasão silenciosa em lote para tentativa que trava no primeiro passo e acende um alarme.
O que fica#
Todo incidente vira um punhado de frases que a gente carrega para o próximo cliente. Deste, ficaram quatro.
- Flag de compatibilidade é dívida de segurança com juros. "Temporário" tem o hábito de virar permanente, e cada caminho legado que sobrevive é uma superfície de ataque esperando alguém religar por engano. Caminho legado precisa de data de validade e de um plano para ser removido, não só desligado.
- Dado de baixa entropia não autentica ninguém. Se um número está numa nota fiscal, ele não protege uma conta. Fluxo de recuperação que depende só disso está pedindo para ser explorado.
- Sem log de origem, não há para onde apontar. Registrar o IP real de quem chama deveria ser o mínimo, não um "corrigimos depois".
- Ataque direcionado a pessoas-chave pede vigilância diferente. Quando alguém escolhe as contas a dedo, as defesas de média também precisam ser específicas.
Por que isso importa#
A gente não escreve isso para bater no peito. Escreve porque a diferença entre o susto e o desastre, neste caso, coube em duas decisões de método: investigar antes de mexer, e blindar o cômodo inteiro em vez de só o buraco.
Contenção no mesmo dia é o que aparece no relatório. O que não aparece, e é o que de fato protege o cliente da próxima vez, é o trabalho chato de assumir que, se um caminho falhou, os vizinhos dele também merecem desconfiança.
Segurança boa raramente é heroica. Quase sempre é só isso: alguém disposto a puxar o fio até o fim, mesmo quando o buraco já parece tapado.
