Retour à tous les posts
Recherche

Pourquoi votre scanner remonte 1 200 vulnérabilités et seulement 12 sont vraies

Ouvrez n'importe quel rapport SAST sur une codebase qui tourne. Vous verrez des chiffres à quatre digits en face de 'critique'. Petit guide de ce que reachability, exploitability et logique métier veulent vraiment dire, et pourquoi le pattern matching seul ne sait pas les distinguer.

Sur cette page
  1. Le chiffre 1 200 n'est pas un bug du SAST. C'est son design.
  2. Filtre numéro un : la reachability
  3. Filtre numéro deux : l'exploitability
  4. Filtre numéro trois : la logique métier
  5. Alors comment passe-t-on vraiment de 1 200 à 12 ?
  6. Pourquoi ça compte plus en 2026 qu'en 2022
  7. Petite note de bas de page

Ouvrez n'importe quel rapport SAST sur une codebase qui tourne. Vous verrez des chiffres à quatre digits en face de "critique". Ouvrez le même scan une semaine plus tard. Les mêmes chiffres, à quelques dizaines près. Au bout d'un moment, un ingénieur sécurité noie dans le triage et l'équipe ship quand même. Cet article parle de ce qui se passe vraiment dans ces rapports, et pourquoi la plupart de ce que vous voyez n'est pas ce que ça prétend être.

La première fois qu'un outil SAST vous annonce "1 247 vulnérabilités", une petite voix pose la bonne question : y a-t-il vraiment mille deux cent quarante sept bugs exploitables dans ce dépôt ? Vous savez déjà que la réponse est non. Ce que vous ne savez peut-être pas, c'est exactement pourquoi la réponse est non, ni ce qui devrait être vrai pour que le chiffre veuille dire quelque chose.

C'est un article sur trois filtres : la reachability, l'exploitability et la logique métier. Tout outil d'analyse statique sérieux doit les appliquer, dans un certain ordre. La plupart trichent sur au moins un. Voici ce que fait chaque filtre, avec du code concret, et pourquoi le pattern matching seul continue à produire des rapports à quatre digits, un an après que le rapport à quatre digits a fait abandonner toutes les équipes.

Le chiffre 1 200 n'est pas un bug du SAST. C'est son design.

Un scanner SAST à base de pattern matching fait à peu près ceci : il parse le source, parcourt l'AST, et pour chaque nœud vérifie s'il matche une liste de formes dangereuses. eval(x). exec(x). Une chaîne SQL avec des + entre littéraux. Une assignation à innerHTML =. Chaque match devient un finding.

C'est rapide et portable entre langages. Ça n'a aussi rien à voir avec la question de savoir si quelqu'un peut réellement déclencher la forme dangereuse. Quand le scanner remonte 1 247 findings, ce qu'il vous dit vraiment c'est : "j'ai trouvé 1 247 patterns syntaxiques qui pourraient, en principe, être dangereux dans un contexte d'appel que je n'ai pas pris la peine de regarder."

Cette dernière clause fait beaucoup de travail. Démontons-la.

Filtre numéro un : la reachability

Un finding est reachable quand un attaquant peut effectivement exécuter le code qui le contient.

Regardons ce handler Express.

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

Un scanner par patterns va flagger la ligne db.raw(sql). Injection SQL depuis input utilisateur. Sévérité : critique. Le scanner n'a pas tort sur la forme, c'est bien la forme d'une injection SQL. Mais pour l'exploiter, la requête doit passer requireAdmin. Donc l'attaquant doit déjà être admin. Donc il n'y a pas d'injection SQL ici, il y a un problème "on fait confiance aux admins pour ne pas casser la base", ce qui est une autre conversation.

La reachability dit : depuis n'importe quel point d'entrée contrôlable de l'extérieur (une requête, un message de queue, un payload désérialisé), est-ce que la donnée peut atteindre cette forme dangereuse ? Si la réponse est non, le finding n'est pas réel, même si le pattern syntaxique l'est.

Il y a trois manières classiques de rendre du code unreachable au sens où on l'entend :

  1. Des gates d'authentification ou d'autorisation se placent entre le point d'entrée et le sink. Le middleware court-circuite le chemin dangereux.
  2. Une sanitisation transforme l'input attaquant avant qu'il n'atteigne le sink. Un parseInt qui throw sur du non-numérique. Une clause WHERE construite via une requête paramétrée. Un framework qui auto-escape l'HTML.
  3. La fonction n'est jamais réellement appelée depuis un chemin reachable. Le code mort est une vraie catégorie et les codebases modernes en sont remplies. Du vieux tooling admin. Un handler enregistré sur une route qui n'existe plus. Une fonction de librairie utilisée seulement par les tests.

La version honnête de "1 247 findings" ressemble plutôt à "dont 380 sont reachables depuis au moins un point d'entrée contrôlable de l'extérieur". Vous avez déjà coupé la queue de deux tiers, et vous n'avez même pas commencé à regarder l'exploitability.

Filtre numéro deux : l'exploitability

Reachable ne veut pas dire exploitable. Ça veut juste dire que le chemin de la donnée existe.

Reprenez le même exemple db.raw, sans le middleware d'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);
});

Le pattern est toujours là. Le chemin est toujours reachable depuis n'importe quel client non authentifié. Le finding est toujours "injection SQL". Mais le paramètre lié [\%$%`]veut dire que la base traiteqcomme une valeur, pas comme du SQL. Il n'y a pas d'injection, peu importe ce que contientq`. Le finding n'est pas exploitable.

L'exploitability concerne la forme de la donnée entre la source et le sink. Trois choses comptent ici :

  • Les frontières d'encodage. Une clause WHERE construite avec un binding de paramètres est exploit-safe au niveau SQL. Un moteur de templates avec auto-escape est exploit-safe au niveau HTML. Les scanners par patterns ne voient pas toujours ces frontières parce qu'elles vivent dans des appels de méthodes de framework (db.raw(template, params)) plutôt que dans des patterns syntaxiques.
  • Le rétrécissement de type. Si une valeur passe par un parseInt, un schema Joi ou une type guard TypeScript avant d'atteindre un sink, sa forme est contrainte. L'injection SQL exige le contrôle de la chaîne brute. Un entier typé ne peut pas injecter de SQL.
  • La sémantique du sink. child_process.exec("ls") n'est pas une injection si la chaîne est un littéral. eval(JSON.stringify(x)) est inoffensif même s'il contient le mot eval. C'est le sink qui est dangereux, pas l'appel.

Après ce filtre, le chiffre baisse encore. De 380 findings reachables vous pouvez tomber à 60 réellement exploitables. La plupart des équipes s'arrêtent là, déclarent ces 60 comme le "vrai" backlog, et commencent à trier.

C'est une erreur. Les 60 qui restent sont des bugs CWE. Les bugs qui font mal ne sont pas toujours des bugs CWE.

Filtre numéro trois : la logique métier

Voici une vulnérabilité qu'aucun scanner SAST par patterns ne peut flagger, peu importe sa subtilité :

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 peut être -1
  }
  await charge(req.user, total);
  await fulfilOrder(req.user, items);
});

Mettez quantity à -1 et le prix se soustrait. Empilez des items à quantité négative contre un positif et le total atteint zéro. Le charge(0) réussit. La commande part.

Ce n'est pas dans l'OWASP Top 10. Ce n'est pas un pattern CWE. Il n'y a pas de forme syntaxique qui dit "des quantités négatives peuvent être exploitées" ; la forme total += a * b est de l'arithmétique normale. La vulnérabilité, c'est que le métier autorise des quantités négatives en entrée mais que le métier veut que chaque quantité soit un entier positif. Cette intention vit dans votre codebase, dans des specs produit, dans des tests que personne n'a écrits. Elle ne vit pas dans une base CWE.

Les failles de logique métier comptent pour près de la moitié des brèches qui produisent une vraie perte financière. Empilement de coupons. Escalade de privilège via mauvaise utilisation du workflow. Race conditions dans les transitions d'état des comptes. Le scanner qui a trouvé 1 247 findings syntaxiques a raté toutes ces failles parce qu'il regardait la forme du code, pas l'intention du code.

Pour les attraper, il vous faut des règles minées depuis la codebase elle-même : "ce champ est toujours un entier positif ici, ici et ici, mais le contrôleur ne le valide pas". Ça demande de lire plus que l'AST.

Alors comment passe-t-on vraiment de 1 200 à 12 ?

Le pipeline qui produit 12 à partir de 1 200 n'est pas une regex plus intelligente. C'est une forme d'analyse complètement différente. Grosso modo :

Construire un graphe de la codebase

Symboles, appels de fonction, flows de types, gates de framework, bindings de routes. Pas un flux de tokens à plat. Un graphe que l'analyseur peut parcourir.

Le parcourir pour la reachability et l'exploitability

Depuis chaque point d'entrée, suivre la donnée. S'arrêter aux sanitiseurs, aux gates, aux rétrécissements de type. Seuls les chemins qui survivent au parcours sont des candidats réels.

Faire émerger les règles métier depuis le code

Miner les invariants depuis la codebase : ce champ est toujours positif, cette transition est toujours gatée par un paiement, ce workflow se termine toujours par un audit log. Chaque invariant devient une contrainte que l'agent (ou l'humain) doit satisfaire.

La première étape est ce que la plupart des produits SAST modernes appellent un "code property graph", ou knowledge graph. En construire un est difficile. Le parcourir l'est encore plus. Mais c'est la seule manière honnête de répondre à "ce finding est-il réel ?" sans demander à un ingénieur sécurité de lire toute votre codebase.

Les 12 qui survivent aux trois filtres sont ceux qui méritent un ticket Jira. Les 1 235 qui n'ont pas survécu ne sont pas des "faux positifs" au sens moral du terme ; ce sont des patterns syntaxiques que le scanner n'a pas pu écarter sans graphe. Si votre scanner n'a pas de graphe, vous récoltez les 1 247 chaque lundi.

Pourquoi ça compte plus en 2026 qu'en 2022

En 2022, le rapport à 1 247 findings était pénible. Un ingénieur sécurité le triait lentement, personne n'aimait ça, mais la codebase grandissait à un rythme humain, et la queue aussi.

En 2026 la codebase grandit à un rythme d'agent. Les agents IA de codage écrivent désormais une part significative de chaque ligne qui atterrit en production. Ils produisent du code dans les patterns que l'agent reconnaît, pas dans les patterns que l'ingénieur sécurité reconnaît. Le chiffre 1 247 scale avec la vitesse d'écriture, pas avec la vitesse de triage.

Vous avez deux options. Soit l'analyse devient drastiquement plus intelligente (à base de graphe, filtrée par reachability, exploitability et logique métier avant que les findings ne soient émis), soit la queue vous mange. Il n'y a pas de troisième option où un humain lit tout.

Petite note de bas de page

On a construit CybeDefend autour de cette idée. L'architecture est un code knowledge graph plus une couche de mining de logique métier (qu'on appelle BLSA) plus une interface inline pour que l'analyseur tourne pendant que l'agent écrit, et pas après le merge. Si vous avez lu jusqu'ici et que vous voulez voir les trois mêmes filtres tourner sur votre propre repo, le chemin install-free est la lecture la plus rapide de ce à quoi ressemble votre vrai backlog ; trente minutes du clone au premier verdict, sans CB.

Le chiffre sur le rapport n'est pas le nombre de bugs. C'est le nombre de patterns que le scanner n'a pas pu écarter sans faire plus de travail. Les équipes qui gagnent sont celles dont le scanner fait le travail.

Démarrer

Installation gratuite dans votre IDE. Premier scan en 5 minutes.

Sans carte bancaire. Sans appel de configuration. Choisissez votre agent, collez la commande, Cybe applique vos règles dès le prochain prompt.

Région
claude mcp add cybedefend --transport http https://mcp-eu.cybedefend.com/mcp

MCP hébergé, aucune install. Enregistrez juste l'URL dans votre agent.

Réserver une démo de 20 min