Além do OWASP Top 10: Como identificar vulnerabilidades de segurança no seu código
Todos nós já colocamos em produção um código que passou por todas as checagens. O PR é aprovado, o build vai para produção, e seguimos em frente. O problema é que os bugs mais sérios muitas vezes não estão nesses relatórios. Eles são as vulnerabilidades de segurança que vêm da nossa própria lógica de negócio, das interações entre serviços e das suposições erradas que fazemos sobre como nosso sistema vai ser usado. Esses são os problemas que nenhuma ferramenta pronta de mercado consegue encontrar, porque são únicos daquilo que nós construímos.
Os limites de checklists e scanners
Uma lista de vulnerabilidades conhecidas é uma ótima base. Ela mostra erros comuns que outros desenvolvedores já cometeram, como não sanitizar entradas SQL ou deixar um bucket S3 público. Scanners automatizados são bons em encontrar esses padrões já conhecidos. Eles conseguem ver uma string sendo concatenada direto em uma query de banco de dados e sinalizar aquilo como uma possível injeção de SQL.
O que essas ferramentas não pegam é o contexto. Um scanner não entende o modelo de multi-tenancy da sua aplicação. Ele não sabe que um usuário com role: "viewer" no tenant A nunca deveria nem conseguir ver um recurso do tenant B, mesmo que adivinhe o ID. O scanner só vê uma consulta ao banco que parece correta do ponto de vista de sintaxe. Ele verifica se o código está montado do jeito certo, mas não consegue dizer se a lógica faz sentido em termos de segurança.
É assim que muitas vulnerabilidades passam batido. Nossos code reviews costumam focar em correção, perguntando “Essa funcionalidade funciona como foi especificada?”. Raramente temos uma forma estruturada de fazer a pergunta mais importante: “Como essa funcionalidade poderia ser abusada?”. O resultado é um sistema que funciona bem para o usuário esperado, mas fica exposto para qualquer pessoa que saia desse fluxo.
> EXPERT CODE REVIEW
Scanners found 0 issues. 2 logic flaws remain.
WARNING: Cutting secure logic triggers an explosion.
Quando as vulnerabilidades de segurança são únicas do seu sistema
As vulnerabilidades mais graves quase nunca vêm de uma falha isolada. Elas aparecem nas conexões entre os componentes. Os problemas surgem quando uma parte do sistema faz suposições sobre a outra.
Os problemas mais perigosos geralmente estão em como as coisas interagem. Pense em uma arquitetura de microsserviços em que um OrderService precisa checar as permissões de um usuário. Ele chama o AuthService para validar um token. O AuthService confirma que o token é válido e retorna um userId. O OrderService então confia totalmente nesse userId e usa esse valor para buscar pedidos.
// OrderService
async function getOrder(orderId, authToken) {
// Chama o AuthService para validar o token
const { userId, isValid } = await authService.validateToken(authToken);
if (!isValid) {
throw new Error("Unauthorized");
}
// A vulnerabilidade está aqui. Temos um usuário válido, mas não necessariamente o usuário *certo*.
const order = await db.orders.find({ where: { id: orderId } });
// Deveríamos checar se esse pedido pertence a esse userId.
// if (order.ownerId !== userId) { throw new Error("Forbidden"); }
return order;
}
O bug não está no AuthService nem no OrderService individualmente. Os dois cumprem seu papel corretamente quando vistos de forma isolada. A vulnerabilidade existe no ponto de integração, na suposição não dita de que um token válido de qualquer usuário já é suficiente para buscar qualquer pedido. Isso é um Insecure Direct Object Reference (IDOR) criado por lógica de negócio, não por uma configuração errada do framework.
A lógica específica da sua aplicação é o que cria esses pontos cegos. Outro padrão comum é a manipulação de estado. Imagine um processo de checkout com várias etapas:
- Adicionar um produto ao carrinho (uma assinatura padrão de $10).
- Ir para uma página separada para aplicar um cupom de 50% de desconto. O backend confirma que o cupom é válido para os itens no carrinho e armazena o desconto na sessão.
- Seguir para a página final de pagamento, onde o total é calculado como $5.
Uma falha lógica aparece se o usuário puder voltar para a etapa 1 depois de aplicar o desconto e trocar a assinatura padrão por uma premium que custa $100. Se o sistema não validar de novo o cupom com base no novo conteúdo do carrinho, o usuário segue para o checkout e paga $50 por um produto de $100. Cada etapa do fluxo era segura por conta própria. A vulnerabilidade foi criada pela sequência de operações e pela falta de uma nova verificação de estado.
Como encontrar esses problemas na prática
Encontrar essas vulnerabilidades exige ir além de ferramentas automatizadas e checklists. Você precisa olhar o sistema tentando entender como alguém poderia explorar falhas e questionar as suposições que ficaram no design do seu sistema.
Acompanhando o fluxo dos dados pela sua aplicação
Toda vez que um usuário fornece uma entrada, você deve tratá-la como não confiável. Esse é um princípio básico que muitas vezes esquecemos conforme os dados passam pelos nossos sistemas. Uma boa prática durante o code review é escolher um único dado fornecido pelo usuário e rastrear todo o ciclo de vida dele.
- Onde os dados entram e saem? Siga um parâmetro como
organization_iddesde a requisição inicial da API. - Onde ele é lido pela primeira vez?
Onde ele é usado pela última vez?
Em algum momento ele é escrito em um arquivo de log, onde pode expor informações? Como ele muda no caminho? Ele é convertido ou modificado?
O tipo dele muda de string para integer, criando um possível problema em checagens de igualdade estrita ("123"vs.123)?
Quando esseorganization_idé passado do gateway de frontend para um serviço de backend, o serviço de backend valida de novo se o usuário autenticado realmente pertence àquela organização ou confia cegamente no gateway?
Esse rastreamento manual é cansativo, e por isso mesmo é um bom candidato para receber ajuda. Você pode usar ferramentas com IA para analisar um pull request e mapear automaticamente esses fluxos de dados. Peça para ela “rastrear todo uso do parâmetro request.body.documentId e listar cada função e query de banco de dados que ele alcança.” Isso não vai encontrar a vulnerabilidade por você, mas vai te dar um mapa de todas as áreas de maior risco para inspecionar manualmente.
Identificando suposições de confiança nos pontos de integração
Vulnerabilidades costumam aparecer nas bordas do sistema. Entre serviços, na integração com APIs externas ou no uso de bibliotecas. Em cada uma dessas partes, sempre tem alguma suposição sendo feita. O trabalho é identificar o que está sendo assumido e checar se isso faz sentido.
Quando o Serviço A chama o Serviço B, é comum passar informações de identidade ou permissões do usuário nos headers ou no corpo da requisição, como X-User-ID: 123 ou X-User-Roles: admin. A pergunta é: o Serviço B valida isso em algum momento? Ou qualquer serviço na rede interna poderia chamar o Serviço B direto, mandar X-User-Roles: admin e ganhar acesso total?
Propagar identidade dentro do sistema exige uma base segura, como autenticação entre serviços e uso de tokens assinados.
Um exemplo clássico com APIs externas é um webhook de pagamento. Seu serviço recebe uma requisição de um provedor de pagamento dizendo que um pedido foi pago. O payload contém order_id: "xyz" e status: "SUCCESS". A maioria dos desenvolvedores vai verificar a assinatura criptográfica da requisição para confirmar que ela realmente veio do provedor. Mas muita gente esquece a próxima etapa: validar o estado dentro do próprio sistema. O pedido “xyz” realmente existe? Ele está em estado pending_payment? Um atacante pode reenviar um webhook válido de um pedido antigo para conseguir um produto novo de graça se você não verificar o estado interno da aplicação.
Revisando o uso específico que o seu código faz das dependências
O scan de dependências verifica CVEs conhecidos nas bibliotecas que você usa. Ele não vai dizer se você está usando uma biblioteca totalmente segura de forma insegura. Por exemplo, uma biblioteca de JWT vai verificar corretamente a assinatura de um token, mas validar os claims dentro dele é responsabilidade sua. O seu código precisa checar se o claim aud (audience) corresponde ao seu serviço e se o iss (issuer) é quem você espera. A biblioteca fez o trabalho dela. A segurança da implementação depende totalmente de como você a utilizou.
Esse é outro ponto em que um assistente com IA pode ajudar durante o desenvolvimento. Você pode pedir a um modelo que tenha contexto sobre sua base de código para “revisar meu uso da biblioteca jsonwebtoken neste arquivo e verificar se estou validando os claims de audience e issuer.” Isso vai além de procurar padrões de CVEs conhecidos e começa a analisar a lógica da sua implementação. As sugestões ainda precisam ser verificadas por um desenvolvedor, mas isso pode sinalizar omissões comuns que passariam despercebidas e ajudar você a focar nas decisões de segurança mais importantes.
Uma checagem rápida antes de subir código em produção
Você não consegue reduzir esse tipo de revisão mais profunda a um checklist simples, embora possa adotar uma mudança simples de mentalidade. Antes de aprovar um pull request, pare 30 segundos e faça uma pergunta:
“Se eu fosse um agente malicioso, como eu abusaria dessa mudança?”
Esqueça o fluxo esperado e pense no que pode acontecer fora dele. O que acontece se eu enviar um número negativo? Um UUID de outro tenant? E se eu executar essas duas chamadas de API na ordem errada? Esse exercício rápido de pensamento adversarial muitas vezes revela exatamente os tipos de falhas de lógica de negócio e suposições quebradas que ferramentas automatizadas sempre vão deixar passar. Ele força você a olhar para o seu código como alguém tentando quebrá-lo.