Aller au contenu principal
own2pwn
Revue de code sécurité : ce que l'humain trouve, pas le SAST

Revue de code sécurité : ce que l'humain trouve, pas le SAST

J'ai passé le même dépôt vulnérable deux fois : une revue de code manuelle, puis un scan SAST (semgrep). Les failles trouvées côte à côte, et pourquoi l'une ne remplace pas l'autre.

own2pwn11 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 sinks, pas seulement ce qu'un scanner remonte.

Demander une revue de code

Deux relecteurs regardent le même fichier. Le premier est un humain qui connaît le métier de l'application ; le second est un moteur d'analyse statique qui suit la donnée de sa source à son point d'arrivée. Ils ne cochent pas les mêmes cases. Pour le vérifier autrement qu'en théorie, j'ai pris un dépôt volontairement vulnérable, connu et public, et je l'ai audité deux fois : une revue de code à la main, puis un scan semgrep avec les règles communautaires. Voici les deux listes de failles, l'une à côté de l'autre.

Le résultat n'est pas "l'humain gagne" ni "l'outil gagne". Chacun a trouvé des choses que l'autre a laissées passer, et c'est précisément l'intérêt de l'exercice. Une revue de code sécurité sérieuse combine les deux, parce qu'ils sont aveugles à des endroits différents.

Reproductible

Le dépôt est OWASP NodeGoat (une application Node.js truffée de failles à but pédagogique), figé au commit c5cb68a. Le scan tourne avec semgrep 1.176.0 et des paquets de règles publics. Toutes les commandes et tous les extraits ci-dessous sortent de cette exécution, pas d'un exemple inventé.

Revue de code sécurité : deux méthodes, un seul dépôt

Un audit de code source peut se mener de deux façons. La première est humaine : on ouvre les routes, on suit les entrées, on lit la logique d'autorisation. La seconde est automatisée : un outil SAST (Static Application Security Testing) parcourt le code sans jamais l'exécuter, cherche des motifs dangereux et trace le flux d'une donnée non fiable jusqu'à un point sensible. Les deux répondent à la même question, "où est la faille", mais avec des yeux différents. Pour situer ces familles d'outils les unes par rapport aux autres, l'article sur le SAST, DAST, IAST et l'IA pose le décor ; ici, on reste sur l'analyse statique et la relecture.

NodeGoat est un bon banc d'essai : environ 2 200 lignes de JavaScript côté serveur, assez petit pour être lu entièrement en une session, et volontairement criblé de vulnérabilités qui correspondent aux catégories du Top 10 de l'OWASP. Chaque faille y côtoie son correctif, laissé en commentaire, ce qui rend l'évaluation honnête : on sait ce qui est vulnérable et pourquoi. La commande du scan tient en quelques lignes.

bash
# semgrep 1.176.0, dépôt OWASP/NodeGoat @ c5cb68a
semgrep scan \
  --config p/javascript --config p/nodejs --config p/expressjs \
  --config p/security-audit --config p/owasp-top-ten \
  app/

# Ran 98 rules on 43 files: 10 findings.

Quatre-vingt-dix-huit règles, quarante-trois fichiers, dix résultats. C'est peu, et c'est déjà instructif : sur ces dix, une partie vise du code serveur réellement dangereux, une autre partie tombe sur des fichiers de documentation. Regardons ce qui est remonté.

Ce que le SAST a trouvé seul

Le scanner brille sur les failles qui ont une forme : une entrée de requête qui atterrit sur une fonction dangereuse. Le cas d'école est ici une injection de code par eval, dans le handler des cotisations. Trois lignes, trois findings, chacun classé CWE-95.

javascript
// app/routes/contributions.js
this.handleContributionsUpdate = (req, res, next) => {
    // Insecure use of eval() to parse inputs
    const preTax = eval(req.body.preTax);     // <-- semgrep flag CWE-95
    const afterTax = eval(req.body.afterTax);
    const roth = eval(req.body.roth);
    // ...
};

Le message de semgrep est net : "Found data from an Express or Next web request flowing to eval." C'est exactement le genre de raisonnement qu'un moteur d'analyse de flux fait bien et vite : la donnée part de req.body, elle n'est ni validée ni transformée, elle arrive sur eval. Aucune ambiguïté, aucune fatigue, et ça retombe dans le mille à chaque commit. Le scan a aussi épinglé une redirection ouverte (CWE-601) où l'URL de destination est prise telle quelle dans req.query.url, et un cross-site scripting réfléchi dans un gabarit HTML (CWE-79) où une variable est injectée dans un attribut href. Voici la liste brute, telle qu'elle sort.

semgrep
Fichier et ligneRègle déclenchéeCWE
routes/contributions.js:32Injection dans evalCWE-95
routes/contributions.js:33Injection dans evalCWE-95
routes/contributions.js:34Injection dans evalCWE-95
routes/index.js:72Redirection ouverteCWE-601
views/profile.html:78XSS, variable dans un hrefCWE-79
views/tutorial/a2.html:207Lien HTTP en clairCWE-319
views/tutorial/a2.html:209Lien HTTP en clairCWE-319
views/tutorial/a2.html:210Lien HTTP en clairCWE-319
views/tutorial/a5.html:50Lien HTTP en clairCWE-319
views/tutorial/a5.html:51Lien HTTP en clairCWE-319
Les dix findings de semgrep. Les cinq derniers pointent des liens http:// dans des pages de tutoriel : techniquement CWE-319, mais du bruit dans ce contexte.

Cinq de ces dix findings visent des liens http:// dans des pages de tutoriel. La règle a raison sur le principe (transport en clair, CWE-319), mais elle se déclenche sur du contenu pédagogique, pas sur une faille exploitable. C'est le lot de tout scan : une part de bruit qu'il faut trier. Retenez surtout ce que la machine a vu toute seule : l'eval, la redirection, le XSS de gabarit. Ce dernier point mérite d'être noté, car un relecteur humain pressé aurait très bien pu survoler ce partial HTML sans le voir. L'outil, lui, ne saute aucune ligne.

Ce que la revue manuelle a trouvé seul

Maintenant, la lecture à la main. Elle commence toujours par les routes, parce que c'est là que se décide qui a le droit de faire quoi. La page des allocations expose sa faille dès la déclaration de la route : l'identifiant d'utilisateur est pris dans l'URL.

javascript
// app/routes/allocations.js
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 c'est le cas le plus pur de faille que le SAST ne voit pas : 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. Ce qui cloche, c'est l'intention.

idor-sast-aveugle
Attaquant
GET /allocations/43
Utilisateur connecté sous l'id 42, qui édite l'URL à la main.
Handler
displayAllocations
Passe le userId de l'URL au DAO, sans le comparer à la session.
MongoDB
Requête valide
Aucun appel dangereux. Le SAST ne voit qu'une lecture normale, et se tait.
Le chemin de la donnée est irréprochable pour un moteur d'analyse : une lecture légitime. La faille est dans ce qui manque, pas dans ce qui est écrit.

Le même fichier de données cache une seconde faille, plus vicieuse, que le scan a également ratée : une injection NoSQL. Le filtre de recherche construit une clause $where de MongoDB en concaténant directement le paramètre threshold de l'utilisateur.

javascript
// app/data/allocations-dao.js
if (threshold) {
    return {
        // threshold vient du client, injecté sans échappement
        $where: `this.userId == ${parsedUserId} && this.stocks > '${threshold}'`
    };
}

L'opérateur $where de MongoDB exécute une expression JavaScript côté serveur pour chaque document. Concaténer une entrée utilisateur dedans, c'est offrir l'exécution de JavaScript arbitraire dans le moteur de base. Un threshold valant 0';while(true){}' transforme la requête en déni de service. Cette faille est cousine des injections SQL classiques, mais le sink est spécifique à MongoDB. Les règles communautaires que j'ai chargées connaissent eval ; elles n'avaient pas de motif pour $where. Un relecteur qui sait ce que fait cet opérateur le repère en une seconde.

Enfin, l'authentification. Le stockage et la vérification des mots de passe se font en clair, la version sécurisée avec bcrypt étant restée en commentaire dans le code du dépôt.

javascript
// app/data/user-dao.js  (validateLogin)
const comparePassword = (fromDB, fromUser) => {
    return fromDB === fromUser;              // comparaison en clair
    // Fix : return bcrypt.compareSync(fromDB, fromUser);
};

// app/data/user-dao.js  (addUser)
const user = {
    userName, firstName, lastName,
    password                                  // stocké tel quel
    // Fix : password: bcrypt.hashSync(password, bcrypt.genSaltSync())
};

Aucun appel de fonction dangereuse ici non plus : un === et une affectation d'objet ne déclenchent aucune règle. Il faut comprendre que password doit être haché pour voir le problème. Même logique pour la route d'administration des avantages sociaux : elle vérifie qu'on est connecté, jamais qu'on est administrateur. Le correctif prévu, ajoutant un middleware isAdmin, dort en commentaire.

javascript
// 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);

Pourquoi le SAST rate le contrôle d'accès

Le point commun de l'IDOR, du mot de passe en clair et du isAdmin manquant : ce sont des failles sans sink. Un moteur d'analyse statique raisonne par flux de données. Il part d'une source (une entrée non fiable), suit ses transformations, et alerte quand elle atteint un sink (une fonction sensible : eval, une requête SQL, une écriture de fichier). Ce modèle est redoutable pour les injections. Il est structurellement aveugle au contrôle d'accès, parce qu'un contrôle d'accès manquant n'est pas une ligne de code fautive : c'est une ligne absente. On ne peut pas faire correspondre un motif à quelque chose qui n'existe pas.

C'est aussi pour cela que le contrôle d'accès défaillant est en tête du Top 10 de l'OWASP, catégorie A01 : c'est à la fois la faille la plus répandue et l'une des plus difficiles à détecter par un outil, faute de critère automatique. Décider que /allocations/43 est interdit à l'utilisateur 42 suppose de connaître le modèle d'autorisation de l'application, que l'outil ignore. Le secure coding repose donc sur une discipline humaine que le scanner ne remplace pas : traiter tout identifiant venu du client comme suspect, et rattacher chaque objet à la session qui a le droit de le lire. Poser ces règles au bon moment relève du modèle de menaces, en amont du code.

Pourquoi la revue manuelle rate des sinks

L'inverse est vrai aussi, et il faut être honnête sur ce point. Le XSS de gabarit que semgrep a épinglé se trouvait dans un fichier HTML, ligne 78, noyé au milieu de balises. Sur un dépôt de deux mille lignes, un humain concentré le voit ; sur cinquante mille, après trois heures de lecture, il glisse dessus. La machine ne connaît pas la lassitude, ne saute pas de fichier, et applique la même règle à la première comme à la dix-millième ligne. C'est précisément ce que la relecture humaine fait mal : la couverture exhaustive et régulière.

Il y a une nuance à ne pas gommer : le $where que le scan a manqué n'est pas hors de portée d'un SAST par nature. Une règle dédiée à cet opérateur, ou un moteur avec une base de sinks NoSQL plus riche, l'aurait attrapé. Ce que la mesure montre, c'est que les règles génériques prêtes à l'emploi ne couvrent pas tout : la qualité d'un scan dépend du jeu de règles, et personne n'avait écrit celle-là pour ce code. L'IDOR et le contrôle d'accès, eux, restent hors de portée quel que soit le jeu de règles, parce qu'ils n'ont pas de sink du tout. La différence est là.

delta
SAST seulLes deuxRevue manuelle seule
XSS de gabarit (partial HTML)Injection de code via eval (CWE-95)IDOR sur les allocations
Liens http en clair (bruit)Redirection ouverte (CWE-601)Contrôle d'accès manquant
Mot de passe stocké en clair
Injection NoSQL via $where
SSN et RIB non chiffrés
Le partage des failles sur NodeGoat. À gauche ce que chaque approche voit seule ; au centre ce que les deux attrapent.

La lecture de ce partage tient en une phrase : la colonne centrale, ce sont les failles à sink, que les deux méthodes attrapent ; les colonnes latérales, ce sont les angles morts respectifs. Le SAST couvre le volume et ne fatigue pas ; la relecture couvre l'intention et le métier. Retirer l'une des deux, c'est garder un angle mort entier.

Combiner les deux : la revue de code en pratique

Les code review best practices qui tiennent la route ne choisissent pas entre l'outil et l'humain, elles les empilent. Concrètement :

  • Le SAST est le plancher, dans la CI. Il tourne à chaque commit, casse la build sur un sink introduit, et sert de mémoire qui n'oublie rien. Le brancher tôt évite de découvrir une injection en fin de sprint ; c'est tout l'objet d'un SAST et DAST intégrés à la CI/CD.
  • La relecture humaine se concentre là où l'outil est aveugle. Une fois le plancher automatisé posé, le temps humain, qui est cher, va au contrôle d'accès, à la logique métier et à la gestion des secrets, pas à re-chercher des eval que le scanner voit déjà.
  • Le modèle de menaces oriente la lecture. Savoir quelles données sont sensibles et qui a le droit d'y toucher transforme une relecture ligne à ligne en une chasse ciblée. C'est le même réflexe que 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 du travail 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 pourquoi l'accès au code change tout ce qu'on peut trouver. Et le tri des failles remontées, leur priorisation et leur suivi, relèvent ensuite de la gestion des vulnérabilités : trouver n'est que la première moitié du travail.

À retenir

  • Sur le même dépôt, semgrep a trouvé les failles à sink (injection eval, redirection ouverte, XSS de gabarit) et une part de bruit ; la revue manuelle a trouvé l'IDOR, le mot de passe en clair, l'injection $where et l'absence de contrôle d'administrateur.
  • Un SAST est aveugle au contrôle d'accès par construction : une autorisation manquante est une ligne absente, pas un motif à détecter.
  • Une relecture humaine rate des sinks noyés dans le volume par fatigue ; l'outil, lui, ne saute aucune ligne et tourne à chaque commit.
  • La qualité d'un scan dépend du jeu de règles : le $where manqué l'était par manque de règle dédiée, pas par impossibilité de principe. Le contrôle d'accès, lui, reste hors de portée.
  • En pratique : le SAST en plancher automatisé dans la CI, la relecture humaine sur le contrôle d'accès et la logique métier, le tout orienté par un modèle de menaces.

Si vous voulez une lecture humaine de votre code, celle qui 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. Et pour le plancher automatisé qui tourne à chaque commit, le module SAST de SecAI s'en charge en continu. 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.

Revue de code manuelle ou SAST : lequel choisir ?

Les deux, pas l'un contre l'autre. Le SAST tourne à chaque commit, ne fatigue jamais et couvre tout le dépôt : c'est le plancher. La revue manuelle trouve ce que l'outil 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. Un SAST sans relecture humaine laisse passer des IDOR ; une relecture sans SAST rate des sinks noyés dans le volume.

Qu'est-ce qu'un SAST détecte, et qu'est-ce qu'il rate ?

Un SAST excelle sur les failles qui ont une forme reconnaissable dans le code : une donnée de requête qui arrive sur eval, une redirection ouverte, une concaténation dans une requête SQL, un secret en dur. Il rate ce qui n'a pas de sink : un contrôle d'accès manquant, un identifiant pris dans l'URL au lieu de la session, une règle métier contournable. Ces failles sont du code parfaitement valide, elles ne se distinguent que par l'intention.

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, l'abonnement paie une couverture automatisée à chaque commit.

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.

Quels sont les bons réflexes d'une code review sécurité ?

Suivre chaque entrée non fiable jusqu'à son point d'arrivée, vérifier que chaque route sensible contrôle non seulement l'authentification mais l'autorisation, traiter tout identifiant venu du client comme suspect, et confirmer que les secrets et mots de passe ne transitent jamais en clair. Un SAST branché dans la CI sert de filet permanent ; la relecture se concentre alors sur le contrôle d'accès et la logique métier, là où l'outil est aveugle.

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é.

Articles liés