Voltar a todos os artigos
Pesquisa

Porque é que o teu scanner reporta 1.200 vulnerabilidades e só 12 são reais

Abre qualquer relatório SAST e vês centenas de bandeiras vermelhas. Guia sobre alcançabilidade, explorabilidade e lógica de negócio, e a confusão entre elas.

Nesta página
  1. O 1.200 não é um bug do SAST. É o desenho.
  2. Filtro um: alcançabilidade
  3. Filtro dois: explorabilidade
  4. Filtro três: lógica de negócio
  5. Então como é que se vai de 1.200 para 12?
  6. Porque é que isto importa mais em 2026 do que em 2022
  7. Uma pequena nota de pé de página

De 1.200 descobertas SAST, só 12 chegam a um sink explorável: a análise de fluxo de dados separa o sinal do ruído.

Abre qualquer relatório SAST sobre uma base de código em produção. Vais ver números de quatro dígitos ao lado da palavra «crítico». Abre a mesma análise uma semana depois: os mesmos números, com uma ou outra diferença. No fim, um engenheiro de segurança afoga-se na triagem e a equipa faz deploy de qualquer maneira. Este artigo é sobre o que está realmente a acontecer nesses relatórios, e sobre porque é que a maior parte do que vês não é o que diz ser.

A primeira vez que vês uma ferramenta SAST dizer «1.247 vulnerabilidades», uma vozinha faz a pergunta certa: existem mesmo mil duzentos e quarenta e sete bugs exploráveis neste repositório? Já sabes que a resposta é não. O que talvez não saibas é exatamente porque a resposta é não, e o que teria de ser verdade para o número significar algo.

Este texto é sobre três filtros: alcançabilidade, explorabilidade e lógica de negócio. Qualquer ferramenta de análise estática que valha a pena tem de os aplicar, numa ordem qualquer. Quase todas fazem cábulas em pelo menos um. Aqui está o que cada filtro faz, com código concreto, e porque é que a correspondência de padrões pura e simples continua a produzir relatórios de quatro dígitos um ano depois de um relatório de quatro dígitos fazer qualquer equipa desistir.

O 1.200 não é um bug do SAST. É o desenho.

Um scanner SAST de padrões faz mais ou menos isto: analisa o código-fonte, percorre a AST e, em cada nó, verifica se corresponde a uma lista de formas perigosas. eval(x). exec(x). Uma string SQL com + entre literais. Uma atribuição a innerHTML. Cada correspondência torna-se uma descoberta.

É rápido e portável entre linguagens. É também completamente alheio a saber se alguém consegue disparar essa forma perigosa. Quando o scanner reporta 1.247 descobertas, o que na verdade está a dizer é: «encontrei 1.247 padrões sintáticos que poderiam, em princípio, ser perigosos num contexto de chamada que não me dei ao trabalho de olhar».

É essa última frase que carrega tudo. Vamos desmontá-la.

Filtro um: alcançabilidade

Uma descoberta é alcançável quando um atacante consegue realmente executar o código que a contém.

Olha para este handler de Express.

app.post("/admin/migrate", requireAdmin, async (req, res) => {
  const { sql } = req.body;
  const result = await db.raw(sql);   // SAST flags this
  res.json(result);
});

Um scanner de padrões vai marcar a linha db.raw(sql). Injeção SQL a partir de input de utilizador. Severidade: crítica. O scanner não se engana quanto à forma; é aquela a forma de uma injeção SQL. Mas para a explorar, o pedido tem de passar pelo requireAdmin. O que significa que o atacante já tem de ser admin. O que significa que aqui não há injeção SQL, há um problema de «confiamos que os admins não estraguem a base de dados», que é outra conversa.

A alcançabilidade pergunta: a partir de qualquer ponto de entrada controlável do exterior (um pedido, uma mensagem de fila, um payload desserializado), os dados conseguem chegar a essa forma perigosa? Se a resposta é não, a descoberta não é real, ainda que o padrão sintático o seja.

Há três formas habituais de o código ser inalcançável neste sentido:

  1. Portas de autenticação ou autorização ficam entre o ponto de entrada e o sink. O middleware corta o caminho perigoso.
  2. Sanitização transforma o input do atacante antes de chegar ao sink. Um parseInt que rebenta com o que não seja dígito. Um WHERE construído com consulta parametrizada. Um framework que faz escape de HTML automaticamente.
  3. A função nunca é chamada a partir de nenhum caminho alcançável. O código morto é uma categoria real e as bases de código modernas estão cheias dele. Ferramentas de admin antigas. Um handler registado numa rota que já não existe. Uma função de biblioteca só usada pelos testes.

A versão honesta de «1.247 descobertas» parece-se mais com «das quais 380 são alcançáveis a partir de pelo menos um ponto de entrada controlável do exterior». Já cortaste a fila em dois terços e ainda não começaste a olhar para a explorabilidade.

Filtro dois: explorabilidade

Alcançável não quer dizer explorável. Quer apenas dizer que o caminho de dados existe.

Pega no mesmo exemplo do db.raw, sem o middleware de auth:

app.post("/api/search", async (req, res) => {
  const { q } = req.body;
  const result = await db.raw(
    "SELECT id, title FROM articles WHERE title LIKE ?",
    [`%${q}%`]
  );
  res.json(result);
});

O padrão continua lá. O caminho continua alcançável por qualquer cliente não autenticado. A descoberta continua a dizer «injeção SQL». Mas o parâmetro ligado faz com que a base de dados trate q como um valor e não como SQL. Não há injeção, independentemente do que q contenha. A descoberta não é explorável.

A explorabilidade é sobre a forma que os dados tomam entre a origem e o sink. Contam três coisas:

  • Fronteiras de codificação. Um WHERE construído com parâmetros ligados é à prova de exploit na camada SQL. Um motor de templates com escape automático é-o na camada HTML. Os scanners de padrões não vêem sempre essas fronteiras porque vivem em chamadas do framework (db.raw(template, params)) e não em padrões sintáticos.
  • Estreitamento de tipos. Se um valor passa por um parseInt, um esquema Joi ou um type guard de TypeScript antes do sink, a sua forma fica restringida. A injeção SQL exige controlo sobre a string bruta. Um inteiro tipado não injeta SQL.
  • Semântica do sink. child_process.exec("ls") não é uma injeção se a string for um literal. eval(JSON.stringify(x)) é inofensivo mesmo contendo a palavra eval. O sink é perigoso; a chamada não é.

Depois deste filtro o número volta a cair. De 380 descobertas alcançáveis podes ficar em 60 realmente exploráveis. A maioria das equipas para aqui, declara esses 60 o backlog «real» e começa a triar.

É um erro. Os 60 que te restam são bugs com CWE. Os bugs que doem não têm sempre CWE.

Filtro três: lógica de negócio

Esta é uma vulnerabilidade que nenhum scanner SAST consegue marcar por padrões, por esperto que seja:

app.post("/checkout", requireAuth, async (req, res) => {
  const { items } = req.body;
  let total = 0;
  for (const it of items) {
    const product = await db.products.findById(it.productId);
    total += product.price * it.quantity;   // it.quantity can be -1
  }
  await charge(req.user, total);
  await fulfilOrder(req.user, items);
});

Põe quantity a -1 e o preço subtrai-se. Empilha artigos de quantidade negativa contra um positivo e o total chega a zero. O charge(0) passa. A encomenda sai.

Isto não está no OWASP Top 10. Não é um padrão CWE. Não existe forma sintática que diga «quantidades negativas podem ser exploradas»; a forma total += a * b é aritmética normal. A vulnerabilidade é que o negócio permite quantidades negativas à entrada enquanto o negócio pretende que toda a quantidade seja um inteiro positivo. Essa intenção vive na tua base de código, em especificações de produto, em testes que ninguém escreveu. Não vive numa base de dados de CWE.

As falhas de lógica de negócio estão por trás de quase metade das brechas que produzem perdas financeiras reais. Empilhamento de cupões. Escalada de privilégios por abuso de um fluxo. Condições de corrida em transições de estado de conta. O scanner que encontrou 1.247 descobertas sintáticas falhou todas estas porque estava a olhar para a forma do código, não para a sua intenção.

Para as apanhar são precisas regras extraídas da própria base de código: «este campo é sempre um inteiro positivo aqui, aqui e aqui, mas o controlador não o valida». E isso exige ler mais do que a AST.

Então como é que se vai de 1.200 para 12?

O pipeline que produz 12 a partir de 1.200 não é uma regex mais esperta. É outra forma de análise por completo. Em traços largos:

Constrói um grafo da base de código

Símbolos, chamadas de funções, fluxos de tipos, portas do framework, bindings de rotas. Não um fluxo plano de tokens. Um grafo que o analisador possa percorrer.

Percorre-o à procura de alcançabilidade e explorabilidade

A partir de cada ponto de entrada, segue os dados. Para em sanitizadores, portas e estreitamentos de tipo. Só os caminhos que sobrevivem ao percurso são candidatos reais.

Tira as regras de negócio do próprio código

Extrai invariantes da base de código: este campo é sempre positivo, esta transição passa sempre por um pagamento, este fluxo acaba sempre num log de auditoria. Cada invariante torna-se uma restrição que o agente (ou a pessoa) tem de cumprir.

O primeiro passo é o que a maioria dos produtos SAST modernos chama «grafo de propriedades do código» ou grafo de conhecimento. Construí-lo é difícil. Percorrê-lo, mais. Mas é a única forma honesta de responder «esta descoberta é real?» sem obrigar um engenheiro de segurança a ler a tua base de código toda.

As 12 que sobrevivem aos três filtros são as que merecem um ticket de Jira. As 1.235 que não sobreviveram não são «falsos positivos» em nenhum sentido moral; são padrões sintáticos que o scanner não conseguiu descartar sem um grafo. Se o teu scanner não tem grafo, recebes as 1.247 completas todas as segundas-feiras.

Porque é que isto importa mais em 2026 do que em 2022

Em 2022 o relatório de 1.247 descobertas era irritante. Um engenheiro de segurança triava-o devagar, ninguém gostava, mas a base de código crescia a ritmo humano e a fila também.

Em 2026 a base de código cresce a ritmo de agente. Os agentes de código de IA escrevem já uma parte significativa de cada linha que chega a produção. Produzem código nos padrões que o agente reconhece, não nos que o engenheiro de segurança reconhece. O número 1.247 escala com a velocidade da escrita, não com a velocidade da triagem.

Tens duas opções. Ou a análise fica radicalmente mais esperta (baseada em grafo, filtrada por alcançabilidade, explorabilidade e lógica de negócio antes de emitir descobertas), ou a fila come-te. Não há terceira opção em que uma pessoa leia tudo.

Uma pequena nota de pé de página

Construímos a CybeDefend em torno desta ideia. A arquitetura é um grafo de conhecimento do código mais uma camada de mineração de lógica de negócio (chamamos-lhe BLSA) mais uma interface inline para que o analisador corra enquanto o agente escreve, não depois do merge. Se chegaste até aqui e queres ver os mesmos três filtros a correr no teu próprio repositório, o caminho sem instalação é a leitura mais rápida de como é o teu backlog a sério: trinta minutos do clone ao primeiro veredicto, sem cartão.

O número no relatório não é o número de bugs. É o número de padrões que o scanner não conseguiu descartar sem fazer mais trabalho. As equipas que ganham são aquelas cujo scanner faz esse trabalho.

Em direto · acabado de lançar

Instala o VibeDefend em 5 segundos.

Um comando liga cada agente de coding na tua máquina à CybeDefend: as tuas regras de negócio, os teus frameworks de compliance e guards que bloqueiam chamadas destrutivas antes de dispararem.

Instalar em 5 segundosNode 18.17+
npx -y @cybedefend/vibedefend@latest install
Deteta automaticamente
  • Claude CodeClaude Code
  • CursorCursor
  • OpenAI CodexOpenAI Codex
  • WindsurfWindsurf
  • GitHub CopilotVS Code Copilot
Ler o README no npm