Segurança de aplicação tem um problema de marketing. O que aparece em conferência é cadeia de exploit criativa, bug de kernel, coisa que dá filme. O que derruba empresa de verdade é bem mais chato: um endpoint que não confere se o pedido pertence a quem está pedindo.
Passei por e-commerce, infraestrutura de pagamento, viagens, logística e core banking. Os problemas de segurança que apareceram foram quase sempre a mesma família curta de erros. Vale mais dominar essa família do que ler sobre técnica exótica.
Comece pela pergunta que ninguém faz
Antes de qualquer biblioteca: contra quem você está se defendendo, e o que essa pessoa quer?
Não precisa de metodologia formal. Uma hora, um quadro, quatro perguntas:
O que temos aqui que vale a pena roubar? Dado pessoal, dinheiro, acesso a outro sistema, capacidade de computação.
Quem chega perto disso? Usuário anônimo, usuário autenticado de outro tenant, funcionário, integração parceira, quem tem acesso ao banco.
Se essa pessoa quisesse, por onde ela entraria?
O que aconteceria se ela conseguisse, e a gente descobriria como?
Essa última pergunta é a que mais escancara buraco. A maioria dos times não sabe responder como descobriria um vazamento. Se a resposta é “o cliente avisa”, você não tem detecção.
O erro número um, com folga: controle de acesso quebrado
Se você só puder consertar uma categoria, conserte essa.
O padrão é sempre igual. O endpoint autentica o usuário direito, e depois usa o ID que veio na URL sem conferir se aquele recurso é dele.
// quebrado: autentica, mas não autoriza
@Get(":id")
findOne(@Param("id") id: string) {
return this.orderService.findById(id);
}
Trocar o ID na URL e ver o pedido de outra pessoa é o bug mais comum que existe em API REST. Tem nome, IDOR, e não precisa de nenhuma ferramenta pra explorar. Precisa de curiosidade.
A correção não é adicionar um if nesse controller. É tornar impossível esquecer:
// a autorização faz parte da consulta, não é um passo extra que se esquece
@Get(":id")
findOne(@Param("id", ParseUUIDPipe) id: string, @CurrentUser() user: User) {
return this.orderService.findOneForUser(id, user);
}
E dentro do serviço, o escopo entra no where. Não como validação depois de buscar: como parte da busca. Buscar e depois comparar cria a janela onde alguém, um dia, remove a comparação.
Em sistema multi-tenant isso é ainda mais crítico. A pergunta certa não é “o código filtra por tenant?” e sim “existe algum caminho onde ele pode esquecer?”. Se a resposta depende de disciplina de quem escreve a query, é questão de tempo.
Autenticação: o mínimo que resolve
Senha com Argon2id ou bcrypt com custo apropriado. Nunca SHA-256 puro, que é rápido demais justamente pra isso.
Token de acesso curto, refresh token com rotação e detecção de reuso. Se um refresh token já usado aparecer de novo, você tem indício de roubo e deve invalidar a família inteira.
Rate limit no login e no envio de código, por IP e por identidade. Sem isso, um endpoint de OTP de 4 dígitos é uma porta aberta com uma cortina na frente. Seis dígitos, expiração curta, tentativas limitadas por código.
E o detalhe que quase todo mundo erra: a mensagem de erro do login não deve dizer se o e-mail existe. “Credenciais inválidas” para os dois casos. Enumerar usuário é o primeiro passo de qualquer ataque com um pouco de método.
Confie no banco, não no bom senso
Toda garantia que existe só no código da aplicação é uma garantia até o próximo deploy.
Chave única de idempotência: no banco. Relação obrigatória: chave estrangeira. Valor que não pode ser negativo: constraint. Coluna que não deveria ser atualizada: regra ou permissão que impede.
Isso não é desconfiança do time. É reconhecer que código muda de mão e que a pessoa que vai escrever a próxima query não estava na reunião onde a regra foi combinada.
Segredo é um problema de processo
Chave de API no repositório acontece. O que separa incidente de catástrofe é o que vem depois.
Se aconteceu, a ordem é: rotacionar primeiro, sempre. Depois limpar o histórico. Muita gente inverte, faz um force push bonito e deixa a credencial ativa, que é a única parte que o atacante se importa. Remover do Git não invalida nada.
Prevenção que funciona sem esforço: scanner de segredo no pre-commit, e um pipeline que falha quando encontra algo. Gastar dez minutos configurando isso poupa um fim de semana.
Log é vazamento em potencial
Aquele logger.info(JSON.stringify(request.body)) que alguém colocou pra debugar um problema em produção mandou CPF, token e cartão pro provedor de log. Onde ficam retidos por 90 dias. Com acesso de quase todo mundo do time.
Vazamento não precisa de invasor. Precisa de descuido e de uma ferramenta que guarda tudo.
Tenha uma lista de campos que nunca são logados, aplicada no interceptor, não na memória de quem escreve o log.
O ritmo, que vale mais que a ferramenta
Segurança não é fase antes de lançar. É um conjunto de hábitos pequenos:
Toda revisão de código pergunta quem pode chamar isso e com quais dados. Uma linha na descrição do PR já muda o comportamento do time.
Dependência atualizada com regularidade, não quando dá medo. A janela entre a divulgação da falha e a exploração automatizada é medida em horas.
Um teste automatizado para cada regra de autorização importante. Se o teste “usuário A não vê pedido de usuário B” existir na suíte, essa regra não volta a quebrar em silêncio.
O que eu não faria
Não compraria ferramenta antes de ter o básico. Já vi empresa com scanner caro e IDOR em três endpoints. O scanner não pega isso, porque ele não sabe quem deveria ver o quê.
E não trataria segurança como assunto de especialista. Num time pequeno não existe especialista. Existem decisões de arquitetura que evitam categorias inteiras de problema, e elas são tomadas por quem escreve o código.
Esse é um dos temas que eu levo pra palestra, especialmente para times que não têm um time de segurança e precisam se virar bem mesmo assim.