
Revue de code sécurité : le meilleur SAST du banc signale 38 % des
cas sains
J'ai passé sept configurations SAST sur les 2 740 cas de l'OWASP Benchmark. CodeQL trouve les 1 415 failles, mais signale aussi 207 requêtes SQL sûres sur 232. Ce qui reste à relire à la main.
Maxime J15 min de lecture
Une paire d'yeux sur votre code source
Revue de code whitebox par un pentester OSWE : contrôle d'accès, logique métier et tri des alertes, pas seulement ce qu'un scanner remonte.
Demander une revue de codeUne pull request arrive avec une pile d'alertes de l'analyse statique, dont une bonne part sur des requêtes SQL. Lesquelles sont de vraies injections ? La réponse ne se trouve pas dans le rapport de l'outil, elle se trouve dans le code, et c'est exactement là que commence une revue de code sécurité. Pour savoir combien de ce tri on peut déléguer à la machine, j'ai mesuré sept configurations SAST sur un corpus dont on connaît la vérité cas par cas : l'OWASP Benchmark for Java, 2 740 servlets de test, dont 1 415 réellement vulnérables.
Le meilleur du banc, CodeQL avec sa suite security-extended, trouve les 1 415 failles réelles. Toutes. Mais il signale aussi, en moyenne sur les onze catégories du corpus, 38 % des cas sains : sur l'injection SQL, 207 requêtes sûres sur 232. Sur ce corpus, le problème du SAST n'est pas de trouver, c'est de trier. Et le corpus a un angle mort que la revue humaine, elle, doit couvrir : il ne contient aucune faille de contrôle d'accès.
Cadre de la mesure
CodeQL trouve les 1 415 failles, et signale 38 % des cas sains
Le graphique suit la convention des scorecards du projet OWASP Benchmark. En abscisse, le taux de faux positifs : la part des cas sains que l'outil signale quand même. En ordonnée, le taux de vrais positifs : la part des failles réelles qu'il trouve. Les deux sont des moyennes sur les onze catégories. Un outil qui signalerait au hasard tomberait sur la diagonale, où les deux taux sont égaux ; plus un point est haut et à gauche, mieux il trie.
| Série | Point | Taux de faux positifs (%) | Taux de vrais positifs (%) |
|---|---|---|---|
| CodeQL | 38 | 100 | |
| SpotBugs | 53,4 | 97,2 | |
| Semgrep | p/security-audit | 42,1 | 87,1 |
| Semgrep | p/default | 44,1 | 88,1 |
| Semgrep | p/owasp-top-ten | 42,1 | 68,9 |
| SecAI sans LLM, réglé sur ce corpus | 34,6 | 63,8 | |
| SonarQube Community | 11,2 | 42,6 |
Le score qui résume un point s'appelle l'indice de Youden : taux de vrais positifs moins taux de faux positifs, soit la hauteur du point au-dessus de la diagonale. Zéro, l'outil ne fait pas mieux que le hasard ; cent, il est parfait. CodeQL obtient 62,0. Viennent ensuite Semgrep CE, 45,0 avec le pack p/security-audit et 44,0 avec p/default, SpotBugs et FindSecBugs, 43,8, puis SonarQube Community, 31,4, la pré-passe sans IA de SecAI, 29,2, et Semgrep avec le pack p/owasp-top-ten, 26,8.
Entre Semgrep et SpotBugs, l'écart tient dans un ou deux points et change avec le pack de règles ou le seuil choisi : ce ne sont pas des différences de qualité, ce sont des différences de réglage. Le même Semgrep perd 18,2 points en passant de p/security-audit à p/owasp-top-ten, parce que ce dernier ne contient ni les règles d'aléa faible ni celle du cookie sans attribut Secure : 0 cas trouvé sur 218 dans la première catégorie, 0 sur 36 dans la seconde. Le nom d'un pack ne dit pas ce qu'il couvre ; il faut lire la liste de ses règles.
Ce que vaut le chiffre de SecAI
Sur l'injection SQL, le tri est le vrai travail
La moyenne cache des écarts énormes d'une catégorie à l'autre. Sur l'aléa faible ou le cookie sans Secure, presque tout le monde fait un sans faute : la faille tient dans un appel de fonction, il suffit de le reconnaître. Sur les injections, il faut suivre une donnée à travers des branches, des collections et des fonctions utilitaires, et décider si elle reste dangereuse en arrivant. C'est là que les scores s'effondrent.
- CodeQL security-extended
- Semgrep CE p/security-audit
- SonarQube Community
| Catégorie | CodeQL security-extended (points) | Semgrep CE p/security-audit (points) | SonarQube Community (points) |
|---|---|---|---|
| Injection SQL | 10,8 | 24,4 | 0 |
| Injection de commande | 48,8 | 12,1 | 0 |
| Traversée de chemin | 51,1 | 11,7 | 0 |
| XSS | 56,9 | 30,4 | 0 |
| Injection LDAP | 59,4 | 8,8 | 0 |
| Injection XPath | 65 | 28,3 | 0 |
| Frontière de confiance | 44,2 | 9,9 | 0 |
| Hachage faible | 69,2 | 69 | 69 |
| Chiffrement faible | 76,7 | 100 | 76,7 |
| Aléa faible | 100 | 100 | 100 |
| Cookie sans Secure | 100 | 100 | 100 |
L'injection SQL est la catégorie où CodeQL fait son plus mauvais score, 10,8. Il trouve les 272 requêtes vulnérables, mais il signale aussi 207 des 232 requêtes sûres. Les cas sains du Benchmark sont souvent piégés exprès : une condition toujours vraie qui remplace l'entrée par une constante, une valeur lue dans une collection à un index qui n'est pas celui où l'attaquant a écrit. Un moteur qui ne résout pas ces calculs voit un chemin de la requête HTTP jusqu'à la base, et alerte. Un relecteur, lui, lit la condition et passe au cas suivant.
SonarQube Community montre l'autre extrême. Cette édition n'a pas l'analyse de teinte (taint analysis) de Sonar, réservée à SonarQube Server à partir de l'édition Developer et à SonarQube Cloud. Elle ne trouve aucun cas d'injection de commande, de XSS, de traversée de chemin, d'injection LDAP ou XPath, ni de violation de frontière de confiance. Sur le SQL, elle signale toutes les requêtes construites dynamiquement : les 272 vulnérables et les 232 sûres. Taux de vrais positifs de 100 %, taux de faux positifs de 100 %, Youden de zéro. L'outil a raison de dire "cette requête est construite à partir de variables", il ne dit rien de plus.
- requêtes SQL sûres signalées par CodeQLqui trouve par ailleurs les 272 injections réelles
- 207 / 232
- catégories où SonarQube Community ne trouve aucun cascommande, XSS, chemin, LDAP, XPath, frontière de confiance
- 6 / 11
- cas de hachage faible ratés par toutes les configurations sauf CodeQLMD5, lu dans un fichier .properties au lieu d'être écrit dans le code
- 40 / 129
Quarante MD5 cachés dans un fichier de configuration
Le troisième chiffre est le plus parlant pour une revue de code. Quarante des 129 cas de hachage faible passent sous le radar de toutes les configurations sauf CodeQL. Le code y appelle MessageDigest.getInstance() avec un nom d'algorithme qui n'est pas écrit dans le code : il est lu dans benchmark.properties, où la clé hashAlg1 vaut MD5.
// BenchmarkTest00003.java (extrait)
java.util.Properties benchmarkprops = new java.util.Properties();
benchmarkprops.load(...getResourceAsStream("benchmark.properties"));
String algorithm = benchmarkprops.getProperty("hashAlg1", "SHA512");
java.security.MessageDigest md = java.security.MessageDigest.getInstance(algorithm);
# benchmark.properties
hashAlg1=MD5Une règle qui cherche la chaîne "MD5" dans un appel ne voit rien, puisque le seul littéral du code est le SHA512 de repli. CodeQL attrape ces quarante cas avec une requête qui signale tout algorithme non littéral, au prix de 33 alertes sur des cas sains. Un relecteur qui a le dépôt complet sous les yeux, fichiers de configuration compris, ouvre le .properties et tranche. C'est un bon résumé de ce que vaut l'accès au code : la faille n'est pas dans le fichier Java, elle est dans la combinaison du code et de sa configuration.
Ce que le Benchmark ne mesure pas : le contrôle d'accès
Regardez la liste des onze catégories : injections, XSS, traversée de chemin, cryptographie, aléa, cookie. Toutes ont un sink, une fonction sensible où la faille se matérialise. Aucune ne porte sur le contrôle d'accès, alors que c'est la première catégorie du Top 10 de l'OWASP, A01. Ce n'est pas un oubli du projet : on ne sait pas écrire de vérité terrain automatique pour une autorisation manquante, parce que ce qui est légitime dépend du métier de l'application.
Pour voir à quoi ressemble ce genre de faille, il faut un autre terrain. OWASP NodeGoat, une application Node.js volontairement vulnérable où chaque faille côtoie son correctif laissé en commentaire, en donne un exemple propre. La page des allocations expose sa faille dès la déclaration de la route : l'identifiant d'utilisateur est pris dans l'URL.
// app/routes/allocations.js (OWASP NodeGoat)
this.displayAllocations = (req, res, next) => {
// Fix (en commentaire dans le dépôt) : prendre userId dans la SESSION
// const { userId } = req.session;
const { userId } = req.params; // <-- vient de l'URL
const { threshold } = req.query;
allocationsDAO.getByUserIdAndThreshold(userId, threshold, (err, allocations) => {
// ... renvoie les allocations de CE userId, quel qu'il soit
});
};La route est GET /allocations/:userId et elle est bien protégée par un middleware isLoggedIn. Sauf qu'être connecté ne dit rien sur le droit de lire ce compte-là. Un utilisateur authentifié qui change /allocations/42 en /allocations/43 lit le portefeuille de son voisin. C'est une référence directe à un objet non sécurisée, un IDOR, et le code est parfaitement valide : il lit un paramètre, interroge la base, renvoie le résultat. Il n'y a aucune fonction dangereuse à signaler.
userId de l'URL au DAO, sans le comparer à la session.Le même dépôt range d'autres failles de cette famille. La route d'administration des avantages sociaux vérifie qu'on est connecté, jamais qu'on est administrateur ; le middleware isAdmin prévu dort en commentaire. Et les mots de passe sont stockés et comparés en clair, la version avec bcrypt étant restée, elle aussi, en commentaire.
// app/routes/index.js
app.get("/benefits", isLoggedIn, benefitsHandler.displayBenefits);
// Fix A7 (Function Level Access Control), resté en commentaire :
// app.get("/benefits", isLoggedIn, isAdmin, benefitsHandler.displayBenefits);
// app/data/user-dao.js (validateLogin)
const comparePassword = (fromDB, fromUser) => {
return fromDB === fromUser; // comparaison en clair
// Fix : return bcrypt.compareSync(fromDB, fromUser);
};Le point commun de l'IDOR, du isAdmin manquant et du mot de passe en clair : ce sont des failles sans sink. Un moteur d'analyse statique part d'une source (une entrée non fiable), suit ses transformations, et alerte quand elle atteint une fonction sensible. Ce modèle est fait pour les injections, et le Benchmark le mesure bien. Il est structurellement aveugle au contrôle d'accès, parce qu'une autorisation manquante n'est pas une ligne fautive, c'est une ligne absente. Décider que /allocations/43 est interdit à l'utilisateur 42 suppose de connaître le modèle d'autorisation de l'application. Poser ces règles au bon moment relève du modèle de menaces, en amont du code.
NodeGoat cache aussi une injection NoSQL, cousine des injections SQL classiques : le filtre de recherche concatène le paramètre threshold dans une clause $where, que MongoDB évalue comme du JavaScript côté serveur. Celle-là a un sink, donc une règle peut l'attraper, à condition que quelqu'un l'ait écrite et que le pack chargé la contienne. Le cas p/owasp-top-ten, sans règle d'aléa faible, montre qu'il ne faut pas le supposer.
La mesure
Le corpus est l'OWASP Benchmark for Java v1.2, figé au commit 557a684 du 13 mai 2026 : 2 740 cas de test répartis en onze catégories, 1 415 vulnérables et 1 325 sains. La vérité terrain est le fichier officiel expectedresults-1.2.csv du dépôt, jamais la sortie d'un outil. Les exécutions ont eu lieu le 7 octobre 2026, une par configuration, l'une après l'autre, sur une machine locale.
Pour les réglages, une règle simple : la configuration de référence que le projet Benchmark documente quand elle existe, sinon la configuration par défaut. CodeQL 2.27.2 a donc tourné avec la suite security-extended, qui n'est pas sa suite par défaut, sur une base construite en compilant le projet avec Maven. SpotBugs 4.9.6 avec FindSecBugs 1.14.0 a tourné en effort maximal, seuil bas, filtre sécurité, et Semgrep CE 1.179.0 avec le pack p/security-audit, plus p/default et p/owasp-top-ten pour voir l'effet du pack. SonarQube Community Build 26.9 a tourné avec le profil Sonar way. Pour SecAI, seule la pré-passe déterministe (suivi de flux sans modèle de langage) a tourné : le vérificateur LLM qui trie ses candidats en production n'a pas pu être exécuté ce jour-là.
La règle de comptage a été écrite avant toute exécution et vaut pour tous. Une alerte compte pour un cas si elle tombe dans son fichier, et n'est créditée que si la CWE attachée par l'outil correspond à la catégorie du cas. En cas de doute, on ne crédite pas. Pour SonarQube, j'ai repris la correspondance de l'outil officiel du Benchmark, qui range sa règle de hachage faible en CWE-328 ; avec la CWE que SonarQube déclare lui-même (CWE-1240), il tomberait à 25,2. La règle cookie de SecAI repère les 36 cookies sans attribut Secure mais les étiquette CWE-1004 (HttpOnly) : je ne l'ai pas créditée. Le score de chaque configuration est la moyenne des onze catégories, comme sur les scorecards publiées par le projet.
Une contre-expertise a recalculé tous les comptes à partir des sorties brutes avec un code écrit à part, et retombe sur les mêmes chiffres. Elle a aussi corrigé deux choses dans ma première version, toutes deux à l'avantage des autres outils : la correspondance CWE de SonarQube ci-dessus, et le réglage de SpotBugs et de Semgrep, que j'avais d'abord lancés sans la configuration de référence du projet.
Les limites, à garder en tête avant de citer un chiffre :
- Corpus synthétique. Un servlet par cas, une faille par fichier, des constructions récurrentes. Il mesure le suivi de flux sur des motifs connus en Java, pas le comportement sur une vraie base de code ni sur d'autres langages.
- Corpus connu de tous. Le Benchmark est public depuis 2015 : les auteurs de règles le connaissent, plusieurs éditeurs publient leur scorecard, et SecAI a été réglé dessus. Aucun outil n'y est aveugle.
- Une exécution, pas de durées. La machine était partagée avec d'autres bancs et très chargée : les temps d'analyse ne sont pas comparables et je ne les publie pas.
- Éditions gratuites seulement (SecAI mis à part). SonarQube avec analyse de teinte, Semgrep Pro et les SAST commerciaux ne sont pas mesurés. Une autre configuration donnerait d'autres scores.
Combiner l'outil et la relecture
Les chiffres donnent une division du travail assez nette. Le SAST couvre le volume et ne saute aucune ligne ; la relecture trie ce qu'il remonte et couvre ce qu'il ne peut pas voir. En pratique :
- Le SAST est le plancher, dans la CI. Il tourne à chaque commit et sert de mémoire qui n'oublie rien. Choisissez le pack en lisant ses règles, pas son nom : 18,2 points de Youden séparent deux packs du même Semgrep. Le câblage est l'objet d'un SAST et DAST intégrés à la CI/CD.
- Le tri est un travail humain qu'il faut budgéter. Même le meilleur outil du banc signale 38 % des cas sains en moyenne, et près de neuf requêtes SQL sûres sur dix (207 sur 232). Une revue de code commence par écarter ces alertes, cas par cas, en lisant les conditions et les valeurs.
- Le temps humain va là où l'outil est aveugle. Contrôle d'accès, logique métier, configuration lue à l'exécution. C'est le même réflexe face au code généré par IA, où le contrôle d'accès est justement ce que le modèle oublie le plus.
Cette division recoupe la distinction entre les approches de test : un scan automatique est un regard whitebox partiel, une revue humaine est un whitebox complet. L'article sur le pentest black box, white box et grey box détaille ce que l'accès au code change, et celui sur le SAST, DAST, IAST et l'IA situe l'analyse statique parmi les autres familles d'outils. Le tri des failles confirmées, leur priorisation et leur suivi relèvent ensuite de la gestion des vulnérabilités.
À retenir
- Sur les 2 740 cas de l'OWASP Benchmark Java, CodeQL security-extended obtient 62,0 de Youden : il trouve les 1 415 failles réelles mais signale 38 % des cas sains en moyenne.
- Semgrep (45,0 ou 44,0 selon le pack) et SpotBugs avec FindSecBugs (43,8) se tiennent à un ou deux points : le réglage pèse autant que l'outil, et un pack mal choisi coûte 18,2 points.
- SonarQube Community (31,4) n'a pas d'analyse de teinte : aucun cas trouvé dans six catégories sur onze, et toutes les requêtes SQL dynamiques signalées, sûres ou non.
- La pré-passe sans IA de SecAI, notre produit, fait 29,2 alors qu'elle a été réglée sur ce corpus : un score d'entraînement, sous CodeQL, Semgrep et SpotBugs.
- Le corpus ne contient aucune faille de contrôle d'accès : l'IDOR et l'autorisation manquante restent le terrain de la relecture humaine.
Si vous voulez une lecture humaine de votre code, celle qui trie les alertes et attrape les IDOR et les règles métier contournables qu'aucun scanner ne remonte, c'est le métier d'une revue de code whitebox. Pour le plancher automatisé qui tourne à chaque commit, le module SAST de SecAI s'en charge en continu, avec les limites dites plus haut. Pour en parler avec un humain, la page contact est directe.
Questions fréquentes sur la revue de code sécurité
Qu'est-ce qu'une revue de code sécurité ?
C'est la lecture du code source d'une application dans le but de trouver des failles avant qu'un attaquant ne les trouve. Elle porte sur les entrées non fiables, le contrôle d'accès, la gestion des secrets, la cryptographie et la logique métier. Elle se fait à la main par un relecteur, ou de façon automatisée par un outil SAST qui analyse le code sans l'exécuter. Les deux approches sont complémentaires.
Quel outil SAST gratuit détecte le mieux les failles ?
Sur l'OWASP Benchmark for Java v1.2 (2 740 cas), mesuré le 7 octobre 2026, CodeQL 2.27.2 avec la suite security-extended obtient le meilleur score de Youden moyen, 62,0. Suivent Semgrep CE avec le pack p/security-audit (45,0) ou p/default (44,0), SpotBugs avec FindSecBugs en effort maximal (43,8), puis SonarQube Community (31,4). Entre Semgrep et SpotBugs, l'écart tient au réglage plus qu'à l'outil. C'est un corpus synthétique en Java que tous ces outils connaissent depuis 2015 : il classe le suivi de flux sur des motifs connus, pas le comportement sur votre code.
Pourquoi un SAST produit-il autant de faux positifs ?
Parce qu'il raisonne sur le code sans l'exécuter et ne sait pas toujours qu'une branche est morte ou qu'une valeur est constante. Sur le Benchmark, CodeQL trouve les 1 415 failles réelles mais signale en moyenne 38 % des cas sains, et 207 requêtes SQL sûres sur 232. SonarQube Community, sans analyse de teinte (le suivi des données non fiables, réservé aux éditions supérieures), signale toutes les requêtes SQL construites dynamiquement, les 272 vulnérables comme les 232 sûres. Trier ces alertes est le premier travail d'une revue de code.
Revue de code manuelle ou SAST : lequel choisir ?
Les deux. Le SAST tourne à chaque commit, ne fatigue jamais et couvre tout le dépôt : c'est le plancher. La revue manuelle trie ce que l'outil remonte et trouve ce qu'il ne peut pas voir, en particulier le contrôle d'accès et la logique métier, où il n'y a aucune fonction dangereuse à repérer. L'OWASP Benchmark lui-même ne contient aucune catégorie de contrôle d'accès : ses onze catégories sont toutes des failles à point d'arrivée identifiable.
Le SAST peut-il trouver une faille de contrôle d'accès (IDOR) ?
Rarement, et jamais de façon fiable. Un IDOR consiste à lire l'objet d'un autre utilisateur en changeant un identifiant dans l'URL. Le code qui le permet est valide : il lit une entrée, interroge la base, renvoie le résultat. Il n'y a aucun appel dangereux à signaler. Décider quel identifiant est légitime pour telle session relève de la logique métier, que l'outil ne connaît pas. C'est le domaine de la relecture humaine.
Combien coûte un audit de code source ?
Chez own2pwn, une revue de code whitebox est facturée à partir de 600 euros par jour, avec accès au dépôt et aux comptes de test. Un scanner SAST en self-service comme SecAI démarre à 99 euros par mois et tourne en continu dans la CI. Les deux ne couvrent pas le même besoin : le tarif jour paie une lecture humaine du contrôle d'accès et le tri des alertes, l'abonnement paie une couverture automatisée à chaque commit.
Veille sécurité
La suite, une fois par mois
Ce qui bouge vraiment sur la surface d'attaque externe, NIS2 et la sécurité applicative, écrit par le pentester qui signe ces articles. Un envoi par mois, désinscription en un clic.
Votre adresse ne sert qu'à cet envoi. Voir la politique de confidentialité.
Dans le même dossier
appsec
Shadow AI : 49 sociétés du SBF 120 ont déjà vérifié leur domaine chez un éditeur d'IA
Le DNS de 704 organisations lu le 6 octobre 2026 : 49 sociétés du SBF 120 sur 119 ont vérifié leur domaine chez un éditeur d'IA, 2 communes sur 488. Ce que ce jeton prouve, ce qu'il cache, et comment inventorier le shadow AI.
appsec
Vibe coding et sécurité : j'ai cassé une app générée par IA sans exploit
Vibe coding : front React solide, backend grand ouvert. Clé exposée dans le bundle, aucune Row Level Security, RPC sans autorisation. Récit d'une compromission totale par l'API.
appsec
Prompt injection : ce n'est pas un jailbreak, et ça ne se corrige pas
Le jailbreak vise l'alignement du modèle, la prompt injection vise l'application. Structurelle par nature, l'injection indirecte détourne un agent via une page ou un PDF. Les défenses qui tiennent.