Eu costumava pensar que escrever minha própria camada de autenticação era um distintivo de honra. Se você entende de JWTs e consegue fazer o hash de uma senha, quão difícil pode ser? Eu configurei um backend em Node, emiti tokens e achei que estava pronto. O código funcionava. Os testes passavam. Então comecei a ler sobre como ataques reais realmente acontecem, e o chão sumiu sob meus pés. Cada capítulo sobre injeção, enumeração e vazamentos de canal lateral me mandava de volta para o meu editor com uma sensação de desânimo. Meu sistema de autenticação não tinha apenas lacunas; ele tinha quatro portas escancaradas que eu mesmo havia instalado. Aqui está o que eu descobri e exatamente o que eu mudei.
Injeção de SQL via Concatenação de Strings
O primeiro bug era o truque mais antigo do manual. Eu estava pegando a entrada do usuário e jogando-a diretamente em strings SQL. Na minha rota de login, eu pegava o e-mail do corpo da requisição e o concatenava em uma consulta como SELECT * FROM users WHERE email = '${email}'. Parecia inofensivo porque eu controlava o frontend. Essa é uma suposição perigosa. Um invasor não precisa do seu frontend. Um único corpo de POST manipulado poderia transformar essa verificação de login em uma violação de dados ou em um banco de dados apagado. Envie um payload como ' OR '1'='1 ou, pior, uma consulta empilhada que apaga tabelas, e se a string for executada de forma bruta, seus dados sumirão. Eu não tinha dado ao banco de dados nenhuma maneira de distinguir entre o meu código e os dados do invasor.
A correção não foi mais validação de entrada ou escapar strings manualmente. A verdadeira correção foram consultas parametrizadas. Mudei para node-postgres e comecei a usar placeholders como $1. A consulta se torna um template: SELECT * FROM users WHERE email = $1. O driver envia o SQL e os valores por canais separados. O banco de dados trata a entrada estritamente como dado, não importa quais caracteres ela contenha. Essa única mudança fecha toda a classe de ataques de injeção. É mais simples de ler, mais fácil de manter e remove o fardo de ter que ser um mago do regex toda vez que você escreve uma cláusula WHERE.
Enumeração de E-mail através de Mensagens de Erro
Meu segundo erro parecia uma boa UX. Quando um usuário digitava o e-mail errado, eu retornava User not found. Quando acertavam o e-mail, mas erravam a senha, eu retornava Incorrect password. Parecia útil. Também era uma ferramenta de reconhecimento para invasores. Scripts de enumeração podem bombardear seu endpoint de login com milhares de endereços de e-mail. Se o corpo da resposta ou o código de status mudar dependendo se a conta existe ou não, o script pode construir uma lista verificada de seus usuários. Essa lista se torna a base para credential stuffing, phishing direcionado e novas tentativas de força bruta.
Tive que aceitar que a facilidade de uso às vezes precisa perder para a segurança. Alterei todos os caminhos de login mal-sucedidos para retornar exatamente a mesma string: Invalid credentials. Sem dicas. Sem lógica de ramificação na resposta de erro. Quer o e-mail esteja faltando, a senha esteja errada ou a conta esteja bloqueada, o texto permanece idêntico. Isso também se aplica aos fluxos de registro e redefinição de senha; não revele se um endereço já está no seu sistema. Uma mensagem genérica remove um vazamento de informações do qual os invasores dependem.
Ataques de Tempo em Comparação de Senhas
O terceiro bug era invisível. Eu estava
