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.
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],
);
js/sql-injection et la valeur 8.8 apparaissent dans le détail de la règle.
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.
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.
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é.
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.
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.