Aller au contenu principal

Comment tu prouves que tu fais du dev sécurisé

Dire qu'on fait attention à la sécurité ne coûte rien et ne se vérifie pas. Cet article montre comment CodeQL transforme cette intention en artefact opposable : ce que l'analyse de flux repère quand une relecture passe à côté, trois alertes du catalogue officiel en JavaScript, en C# sur .NET 8 et dans un workflow GitHub Actions, le coût réel sur dépôt privé et les angles morts de l'outil.

Sécurité · ·
Adel LATIBI
Adel LATIBI

Le Briefing Dev - les ressources et actus de la semaine, droit dans ta boîte chaque vendredi gratuitement.

En vous inscrivant, vous acceptez de recevoir notre newsletter. Désinscription possible à tout moment.

Toutes les agences web ont une page qui parle de sécurité. Tous les CV de développeur listent les bonnes pratiques quelque part entre Docker et l'anglais courant.

La phrase tient jusqu'au moment où quelqu'un demande de quoi elle est faite. Un client qui envoie un questionnaire fournisseur avant de signer, un recruteur qui creuse après une réponse un peu lisse, un donneur d'ordre qui veut des dates et des traces : la question change de nature dès qu'elle réclame des éléments matériels.

Une déclaration ne se vérifie pas. Une analyse qui tourne sur chaque pull request, des alertes horodatées, un historique de corrections que personne n'a fabriqué la veille du rendez-vous : voilà des éléments qu'on peut montrer à l'écran.

CodeQL, le moteur d'analyse développé par GitHub, produit exactement ce type de trace. La suite explique comment il fonctionne, ce qu'il trouve dans du code qui a pourtant été relu, et où il s'arrête.

Le moment où la question devient gênante

Les questionnaires fournisseurs posent tous une variante de la même demande : décrivez vos pratiques de développement sécurisé et les outils d'analyse que vous utilisez. La case est libre, elle fait trois lignes, et beaucoup d'équipes y écrivent « revue de code par les pairs ». La réponse est honnête. Elle ne prouve rien, parce qu'une revue ne laisse aucune trace exploitable sur ce qui a été cherché.

Cette demande sort du domaine commercial depuis que le règlement européen sur la cyberrésilience est entré dans sa phase opérationnelle. L'obligation de signalement des vulnérabilités activement exploitées s'applique depuis le 11 septembre 2026, et les exigences essentielles portant sur le produit suivront le 11 décembre 2027, d'après le calendrier publié par la Commission européenne. L'article sur ce que le Cyber Resilience Act impose aux éditeurs logiciels détaille le périmètre et les échéances.

Le point commun entre le questionnaire client et le texte réglementaire tient en un mot : la documentation. Ce qui est demandé, c'est un processus qu'on peut inspecter, pas une conviction.

Ce qu'un moteur d'analyse voit et qu'une relecture rate

CodeQL commence par transformer le code en base de données. Les fonctions, les appels, les variables et les chemins d'exécution deviennent des tables interrogeables. Ensuite, des requêtes écrites dans un langage dédié, QL, cherchent des motifs dans cette base.

Trois notions suffisent pour lire une alerte. La source est un point d'entrée contrôlé par l'extérieur : un paramètre de requête HTTP, le titre d'une issue GitHub, un fichier téléversé. Le puits est un endroit dangereux si on y laisse arriver n'importe quoi : une requête SQL, un appel système, une écriture de fichier. L'assainisseur est ce qui coupe le lien entre les deux, comme un passage en paramètre lié.

Un linter regarde une ligne. Une recherche textuelle regarde un mot. L'analyse de flux, elle, suit la donnée à travers les affectations, les appels de fonction et les fichiers, ce qui lui permet de relier une source et un puits séparés par trois niveaux d'indirection et par un utilitaire maison que personne ne soupçonnait.

Source req.query.categorie Propagation construitRequete(filtre) Puits pool.query(sql) Un assainisseur placé n'importe où sur ce chemin fait disparaître l'alerte. L'alerte affiche le chemin complet, étape par étape, pas seulement la ligne fautive.
Le chemin source vers puits reconstitué par l'analyse de flux, tel qu'il apparaît dans une alerte de type path-problem.

Les langages couverts, et celui qui manque

La documentation GitHub liste C et C++, C#, Go, Java et Kotlin, JavaScript et TypeScript, Python, Ruby, Rust, Swift, ainsi que les fichiers de workflow GitHub Actions. Elle précise aussi, en encadré, que les langages absents de cette liste ne sont pas pris en charge, et cite PHP nommément.

Autant l'avoir en tête avant de promettre une analyse à un client : une application Symfony ou WordPress ne produira aucune alerte CodeQL sur son code PHP. Le dépôt sera scanné pour ses fichiers JavaScript et ses workflows, et le reste passera au travers sans le moindre message d'erreur, ce qui est le pire des cas de figure pour qui croit avoir couvert son projet.

Mettre l'analyse en route

Deux chemins existent. La configuration par défaut s'active depuis l'onglet Security du dépôt : GitHub détecte les langages, choisit la suite de requêtes et les événements déclencheurs, puis lance les analyses via GitHub Actions. La configuration avancée génère un fichier de workflow que tu modifies toi-même, ce qui devient nécessaire dès qu'un langage compilé demande une commande de build particulière.

Trois suites de requêtes existent. default tourne en standard. security-extended ajoute des requêtes de précision plus basse. security-and-quality ajoute encore des requêtes de maintenabilité.

Sur la version de l'action, un détail à vérifier : la branche 4 de github/codeql-action est la branche courante, et la version 3 est annoncée comme obsolète pour décembre 2026. Un workflow copié d'un ancien tutoriel ajoutera un avertissement dans tes logs.

Trois alertes, trois corrections

Les exemples ci-dessous sont écrits pour Node.js 22 en modules ES et pour .NET 8 LTS avec Entity Framework Core 8. Les identifiants et niveaux de gravité cités proviennent du catalogue de requêtes publié par GitHub.

Une route Express qui concatène du SQL

import express from "express";
import pg from "pg";
 
const app = express();
const pool = new pg.Pool();
 
app.get("/produits", async (req, res) => {
  const sql = `SELECT nom, prix FROM produit
               WHERE categorie = '${req.query.categorie}'`;
  const { rows } = await pool.query(sql);
  res.json(rows);
});

L'alerte remontée est js/sql-injection, gravité de sécurité 8.8, précision haute, présente dans la suite par défaut. Elle affiche le chemin depuis req.query.categorie jusqu'à l'appel de requête. La correction passe par un paramètre lié :

const { rows } = await pool.query(
  "SELECT nom, prix FROM produit WHERE categorie = $1",
  [req.query.categorie],
);
Annotation CodeQL sur une pull request GitHub : la règle « Database query built from user-controlled sources », de gravité High, signale les lignes qui concatènent req.query.categorie dans une requête SQL
L'alerte telle qu'elle apparaît dans la pull request, posée sur les lignes concernées. L'intitulé affiché est « Database query built from user-controlled sources » ; l'identifiant js/sql-injection et la valeur 8.8 apparaissent dans le détail de la règle.
Vue « Show paths » de l'alerte CodeQL : le chemin de données étape par étape, depuis req.query.categorie jusqu'à l'appel pool.query
Le bouton « Show paths » ouvre le chemin reconstitué par l'analyse de flux. C'est la différence entre une alerte et une preuve : le moteur montre son raisonnement, pas seulement sa conclusion.

Une chose s'ajoute à l'alerte depuis que Copilot Autofix est activé par défaut sur les dépôts publics : sous le message, GitHub propose un correctif rédigé, prêt à être appliqué d'un clic. Sur cet exemple, il propose exactement la requête paramétrée ci-dessus.

Bloc « Suggested changeset » affiché sous l'alerte CodeQL : Copilot Autofix propose de retirer l'interpolation de req.query.categorie de la requête, de la remplacer par un paramètre $1 et de passer la valeur en second argument de pool.query. Sous la suggestion, la mention « Copilot uses AI. Check for mistakes. »
Sous l'alerte, Copilot Autofix propose un correctif applicable d'un clic. L'avertissement du bas est affiché par GitHub, pas par moi : la détection est reproductible, la suggestion ne l'est pas.

La distinction mérite d'être tenue, parce que les deux blocs s'affichent au même endroit et n'ont pas le même statut. L'alerte vient d'un moteur de requêtes déterministe : même code, même version des requêtes, même résultat, par n'importe qui, à n'importe quel moment. C'est ce qui la rend opposable. Le correctif vient d'un modèle génératif nourri de cette alerte ; il n'est pas reproductible et GitHub affiche lui-même un avertissement sous la suggestion. Le premier est une preuve, le second est une aide à la rédaction.

Une API minimale .NET 8 qui appelle FromSqlRaw

Le cas C# est intéressant parce que l'ORM donne une fausse impression de protection. Entity Framework Core paramètre tout seul les requêtes LINQ, et les équipes en déduisent que le SQL brut est sûr lui aussi.

// .NET 8 LTS, EF Core 8, API minimale
app.MapGet("/produits", async (string categorie, CatalogueContext db) =>
{
    var produits = await db.Produits
        .FromSqlRaw("SELECT * FROM Produit WHERE Categorie = '" + categorie + "'")
        .ToListAsync();
 
    return Results.Ok(produits);
});

La bibliothèque CodeQL pour C# modélise les appels d'Entity Framework Core qui exécutent du SQL comme des puits, via une classe dédiée du module EntityFramework. L'alerte est cs/sql-injection, gravité 8.8, précision haute, elle aussi dans la suite par défaut.

Sur EF Core 8, la correction attendue ne passe plus par FromSqlRaw accompagné d'un SqlParameter construit à la main, écriture encore tolérée mais héritée des versions antérieures. Depuis EF Core 7, FromSql prend une chaîne interpolée et convertit chaque valeur interpolée en paramètre de base de données, ce que la documentation Microsoft présente comme sûr par construction :

var produits = await db.Produits
    .FromSql($"SELECT * FROM Produit WHERE Categorie = {categorie}")
    .ToListAsync();

La syntaxe ressemble à une interpolation de chaîne classique. La méthode reçoit en réalité un FormattableString et place un paramètre à l'emplacement de chaque accolade.

Annotation CodeQL sur la pull request .NET : la règle « SQL query built from user-controlled sources », de gravité High, signale l'appel à FromSqlRaw construit par concaténation, et indique que la requête dépend d'un point de terminaison de routage ASP.NET Core
L'alerte sur le code C#. Le message nomme l'origine de la donnée : « this ASP.NET Core routing endpoint ».
Vue « Show paths » de l'alerte C# : deux étapes dans Program.cs, du paramètre categorie marqué Source jusqu'à la concaténation passée à FromSqlRaw, marquée Sink
Le même chemin, côté C# : deux étapes seulement, le paramètre de route puis la concaténation qui atteint FromSqlRaw. Un chemin court n'est pas un chemin évident — c'est le nom de la méthode, et lui seul, qui distingue ici l'appel sûr de l'appel dangereux.

Ce dernier point mérite d'être relevé, parce que je ne l'aurais pas parié. Le paramètre categorie n'est ici qu'un argument de délégué, sans attribut ni contrôleur : rien qui ressemble à la signature d'une action MVC, pour laquelle la modélisation des sources est documentée de longue date. Le moteur reconnaît pourtant l'API minimale et remonte le flux jusqu'à la route. La couverture est plus large que ce que la documentation laisse deviner.

Le correctif automatique, lui, montre mieux qu'en JavaScript pourquoi une suggestion générative ne vaut pas preuve. Copilot Autofix propose ici FromSqlInterpolated. La faille est bien fermée : la méthode paramètre les valeurs interpolées, exactement comme FromSql. Mais c'est le nom d'avant EF Core 7, l'époque où FromSql n'acceptait pas encore de chaîne interpolée. Accepter la suggestion sans la lire ferme donc la vulnérabilité en introduisant une API d'une génération antérieure dans un projet .NET 8. Un correctif juste, et daté.

Bloc « Suggested changeset » sur la pull request .NET : Copilot Autofix propose de remplacer l'appel FromSqlRaw construit par concaténation par un appel FromSqlInterpolated avec une chaîne interpolée. Sous la suggestion, la mention « Copilot uses AI. Check for mistakes. »
La suggestion automatique sur le code C#. Elle ferme la faille, avec une méthode dont le nom a changé deux versions majeures plus tôt.

Un workflow qui affiche le titre d'une issue

Les fichiers de .github/workflows sont analysables comme le reste du code, et cette partie passe souvent inaperçue.

on:
  issues:
    types: [opened]
 
jobs:
  tri:
    runs-on: ubuntu-latest
    steps:
      - run: |
          echo '${{ github.event.issue.title }}'

L'expression est remplacée par sa valeur avant que le shell ne lise la ligne. Un titre d'issue bien choisi exécute donc des commandes sur le runner, avec les secrets du workflow à portée. L'alerte correspondante est actions/code-injection/critical, gravité 9, précision très haute. Le correctif recommandé consiste à passer par une variable d'environnement et à la lire avec la syntaxe du shell :

      - env:
          TITRE: ${{ github.event.issue.title }}
        run: echo "Nouveau ticket : $TITRE"

Écrire ${{ env.TITRE }} dans le run ne corrige rien, puisque la substitution a lieu au même moment. La documentation de la requête montre ce faux correctif comme deuxième exemple fautif.

Annotation CodeQL sur la pull request du workflow : la règle « Code injection », de gravité Critical, signale que le titre du ticket peut être contrôlé par un utilisateur externe via le déclencheur issues. Sous l'alerte, Copilot Autofix propose de passer par une variable d'environnement ISSUE_TITLE lue avec la syntaxe du shell.
L'alerte sur le workflow, en gravité critique. Le correctif proposé automatiquement est celui décrit plus haut : une variable d'environnement, lue par le shell et non par la substitution d'expression.

Ce que tu montres, au juste

Revenons à la question de départ. Une fois l'analyse branchée depuis quelques semaines, voici les quatre éléments que tu peux ouvrir devant un client, un auditeur ou un recruteur, sans rien préparer à l'avance.

  • L'historique des exécutions dans l'onglet Actions, qui montre la date de chaque analyse et le commit associé.
  • La liste des alertes, ouvertes et fermées, avec pour chacune la règle déclenchée, sa gravité et le chemin de données affiché.
  • Les motifs de fermeture saisis par ton équipe, qui documentent les décisions prises sur les cas jugés non exploitables.
  • La règle de protection de branche, qui prouve que le contrôle conditionne la fusion au lieu de rester décoratif.

Ces quatre éléments, tu es le seul à pouvoir les ouvrir

Le détail a son importance au moment de transmettre un dossier plutôt que de le présenter en réunion. Lister les alertes d'un dépôt exige la permission Write, y compris sur un dépôt public. Un client à qui tu envoies un lien vers ton onglet Security reçoit une page d'erreur.

Une seule surface échappe à cette restriction : les alertes affichées sur une pull request, consultables avec un simple accès en lecture. C'est la raison pour laquelle la démonstration de cet article passe par une pull request laissée ouverte et non par une capture de mon tableau de bord.

La logique de ce cloisonnement se comprend. Une alerte sur la branche par défaut décrit une faille réelle et non corrigée dans le code livré ; la rendre publique reviendrait à publier, pour chaque dépôt de la planète, la liste des points d'entrée exploitables avec leur numéro de ligne. Une alerte sur une pull request porte au contraire sur du code proposé, pas encore en production, et la personne qui relit en a besoin pour faire son travail.

C'est exactement la tension que porte le règlement sur la cyberrésilience : démontrer qu'on a fait le travail sans distribuer la carte de ses faiblesses. En pratique, la preuve se montre, elle ne se publie pas — sauf à construire une vitrine pour ça, ce qui demande un geste délibéré.

Cet ensemble a une propriété que n'a aucune déclaration : il est daté et il porte des noms d'auteurs. Personne ne fabrique trois mois d'historique la veille d'un rendez-vous, et c'est précisément ce qui lui donne sa valeur.

Une remarque de terrain sur le calendrier. Brancher l'analyse le jour où un questionnaire arrive produit une liste d'alertes fraîches et aucun historique, ce qui donne exactement l'impression inverse de celle recherchée. La bonne fenêtre pour activer l'outil se situe sur un projet en cours, pendant une période calme, quand le volume d'alertes initial peut être absorbé sans bloquer une livraison.

Écrire une requête à la main

Le catalogue couvre les motifs connus. Les règles maison de ton équipe, non. QL sert à ça, et une première requête tient en quatre lignes :

import javascript
 
from CallExpr appel
where appel.getCalleeName() = "eval"
select appel, "Appel à eval() dans le code applicatif."

La ligne import charge la bibliothèque du langage. CallExpr désigne l'ensemble des appels de fonction du dépôt, where filtre, select renvoie le résultat avec son message.

L'extension CodeQL pour VS Code exécute ce genre de requête sur une base téléchargée depuis un dépôt public, ce qui permet de s'entraîner sur du vrai code sans rien installer côté serveur. Le passage au flux de données demande davantage de travail, et c'est là que la courbe d'apprentissage se redresse franchement.

Les pièges

Prendre zéro alerte pour une preuve d'innocuité

Une analyse statique cherche des motifs. Un contrôle d'autorisation manquant sur une route d'administration ne ressemble à aucun motif : le code est correct, c'est la règle métier qui est absente. Les failles logiques et les mauvaises configurations d'infrastructure sortent du périmètre.

Le périmètre a aussi des bords à l'intérieur même des motifs couverts. Relevé effectué le 16 septembre 2026, sur un dépôt public en suite security-extended : cinq écritures d'une même injection dans un workflow GitHub Actions, toutes exploitables, deux signalées. L'écriture la plus répandue, sur une seule ligne et entre guillemets doubles, ne l'était pas. Cinq cas ne font pas une règle et une version ultérieure du moteur les signalera peut-être toutes, mais la conclusion pratique tient : une alerte prouve ce que le moteur a trouvé, son absence ne prouve rien.

Confondre les trois outils de l'onglet Security

CodeQL analyse ton code. Dependabot surveille les versions de tes dépendances et signale celles qui portent une vulnérabilité connue. Le secret scanning cherche des identifiants publiés par erreur dans l'historique Git. Ces briques répondent à des risques distincts et aucune ne couvre le terrain des autres.

Découvrir la facture après coup

L'analyse est gratuite sur les dépôts publics. Sur des dépôts privés d'organisation, elle relève de GitHub Code Security, affiché à 30 dollars par committer actif et par mois sur le calculateur de tarifs de GitHub. La documentation précise qu'un committer est compté actif si l'un de ses commits a été poussé dans les 90 derniers jours, ce qui inclut le prestataire venu corriger un bug six semaines plus tôt et reparti depuis.

Fermer les alertes sans justification

Les faux positifs existent, en particulier quand une donnée est validée par un utilitaire que le moteur ne modélise pas. Une alerte fermée avec un motif écrit reste consultable et constitue une trace de décision. Une alerte fermée à la chaîne un vendredi soir détruit la valeur probante de l'ensemble.

Activer l'analyse sans bloquer la fusion

Une analyse dont le résultat n'empêche rien devient un rapport que personne n'ouvre. Le mécanisme actuel s'appelle les rulesets, dans les réglages du dépôt, et il propose une règle taillée pour ce cas : Require code scanning results, qui demande l'outil concerné et le niveau de gravité à partir duquel la fusion est refusée. À côté, Require a pull request before merging ferme la porte du push direct sur la branche par défaut, la vôtre comprise.

L'ensemble transforme l'outil en garde-fou plutôt qu'en tableau de bord. C'est aussi ce qui permet de répondre à la question du questionnaire fournisseur avec une phrase vérifiable plutôt qu'avec une intention : la règle est inscrite dans les réglages du dépôt, elle est datée, et elle s'applique à tout le monde.

Reste un détail qui se découvre à l'usage : ajouter un langage à l'analyse ne l'ajoute pas aux contrôles bloquants. La liste des contrôles obligatoires est figée au moment où on l'écrit. Le jour où le dépôt gagne un projet .NET à côté de son JavaScript, la nouvelle analyse tourne et remonte ses alertes, mais la règle continue de ne surveiller que l'ancienne, et rien ne le signale. Beaucoup d'équipes croient leur règle de protection complète alors qu'elle ne couvre qu'un des langages du dépôt. Le contrôle s'ajoute à la main dans le ruleset, et il faut y repenser à chaque langage introduit.

Pour la partie théorique, l'article sur CSRF, XSS et SQLi expliquées avec du code qui casse montre les mêmes familles de failles du côté de l'attaque, et celui sur les en-têtes CSP et HSTS couvre ce qui se joue après le déploiement. Côté pratique encadrée, la formation Git, GitHub Actions et Cyber Resilience Act sur stack .NET monte la chaîne complète, et la formation CI/CD avec GitHub Actions pose les bases du pipeline sur lequel tout cela se greffe.

Questions fréquentes

CodeQL fonctionne-t-il sur un projet PHP ?

Non. La documentation GitHub cite PHP parmi les langages non pris en charge. Le dépôt sera quand même analysé pour ses autres langages, JavaScript et workflows compris, sans message signalant que le code PHP a été ignoré. Pour un projet Symfony ou Laravel, il faut regarder du côté d'autres analyseurs statiques.

Faut-il payer pour analyser un projet personnel ?

L'analyse de code est disponible sans frais sur les dépôts publics de GitHub.com. Pour des dépôts privés appartenant à une organisation, elle demande GitHub Code Security, facturé au committer actif. Un portfolio public reste donc analysable gratuitement.

Combien de temps prend une analyse ?

Cela dépend de la taille du dépôt et du langage, les langages compilés demandant une étape de build. GitHub documente d'ailleurs le cas des analyses trop longues et propose des runners plus puissants ainsi qu'un mode incrémental sur les pull requests. Sur un petit projet interprété, l'analyse reste dans l'ordre de grandeur d'un job de test.

Est-ce que CodeQL suffit pour être conforme au Cyber Resilience Act ?

Non, et aucun outil ne le permet à lui seul. Le règlement porte sur un ensemble qui va de la documentation technique à la gestion des vulnérabilités sur toute la durée de support, en passant par l'inventaire des composants. Une analyse automatisée alimente une partie de ce dossier, elle ne le remplace pas.

Que répondre quand une alerte est un faux positif ?

L'interface propose de fermer l'alerte en indiquant un motif, par exemple un risque accepté ou une utilisation dans du code de test. Écris la raison dans le commentaire associé. Si le même motif revient parce qu'une fonction de validation maison n'est pas reconnue, l'éditeur de modèles de l'extension VS Code permet de la déclarer comme assainisseur.

Par où commencer quand on débute ?

Active la configuration par défaut sur un projet personnel public, laisse tourner, puis ouvre la première alerte et lis le chemin affiché avant de corriger quoi que ce soit. Comprendre pourquoi le moteur relie ces deux lignes apporte plus que la correction elle-même, et cette lecture se transpose ensuite à n'importe quel autre analyseur.

Vous êtes expert ?

Partagez votre expertise sur notre blog

Tutoriel, retour d'expérience, analyse - publiez un article invité et gagnez en visibilité.

Écrire pour nous