Chapitre 1417 minIntermédiaire
Tests de sécurité : revue de code, SAST, DAST et SCA
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- choisir la famille de tests adaptée à une faille ;
- mener une revue de code orientée sécurité ;
- lancer Semgrep, npm audit, gitleaks et ZAP ;
- intégrer les contrôles dans l'intégration continue ;
- rédiger une fiche de vulnérabilité.
3 exercices corrigés1 schéma
Les tests de sécurité vérifient qu'une application résiste aux attaques connues, avant qu'un attaquant ne s'en charge. On combine plusieurs familles : la revue de code par un humain, l'analyse statique du code (SAST), l'analyse des dépendances (SCA), la recherche de secrets, l'analyse dynamique de l'application en fonctionnement (DAST) et le test d'intrusion. Ce chapitre présente chacune, avec les outils courants (Semgrep, npm audit, gitleaks, ZAP), leur intégration dans l'intégration continue et la rédaction d'un rapport d'audit.
Les familles de tests
| Famille | Ce qu'elle examine | Quand | Outils courants | Ce qu'elle trouve bien | Ce qu'elle rate |
|---|---|---|---|---|---|
| Revue de code | Le code source, lu par un humain | À chaque pull request | GitHub, GitLab | Logique métier, contrôle d'accès, défauts de conception | Ce que le relecteur ne connaît pas ; elle dépend de son attention |
| SAST (Static Application Security Testing) | Le code source, analysé sans l'exécuter | À chaque commit | Semgrep, SonarQube, CodeQL, eslint-plugin-security | Injections, fonctions dangereuses, secrets, mauvaises pratiques connues | Le contexte d'exécution, la configuration ; faux positifs |
| SCA (Software Composition Analysis) | Les dépendances | À chaque commit et en continu | npm audit, Dependabot, OWASP Dependency-Check | CVE connues dans les bibliothèques | Les failles de ton propre code |
| Recherche de secrets | Le code et l'historique Git | Avant chaque commit et en continu | gitleaks, GitHub secret scanning | Clés, mots de passe, jetons commités | Les secrets dans un format inconnu |
| DAST (Dynamic Application Security Testing) | L'application en fonctionnement, vue de l'extérieur | Sur un environnement de test ou de préproduction | ZAP, Burp Suite | En-têtes, cookies, XSS, injections, configuration du serveur | Le code jamais atteint par le scan, la logique métier |
| Tests automatisés de sécurité | Des cas précis, écrits par l'équipe | À chaque commit | Vitest, Jest, PHPUnit, JUnit avec Supertest | Les régressions sur le contrôle d'accès, la validation | Ce qu'on n'a pas pensé à tester |
| Test d'intrusion | Toute l'application, par un expert qui l'attaque | Avant une mise en production importante, puis régulièrement | Les outils précédents et l'expertise humaine | Les enchaînements de failles, la logique métier | Il est ponctuel : l'application change ensuite |
Aucune famille ne suffit seule. Les outils automatiques trouvent vite et souvent les failles « techniques » ; seuls les humains trouvent les failles de logique (une IDOR, une étape de validation contournable) que le code exprime parfaitement.
La revue de code orientée sécurité
Une revue de code suit une liste de points de contrôle. Celle-ci reprend les chapitres du cours :
| Point | Ce qu'on cherche dans le code |
|---|---|
| Entrées | D'où vient chaque donnée ? Est-elle validée côté serveur (type, longueur, format) ? |
| Requêtes | Une saisie est-elle concaténée dans du SQL, une requête NoSQL, une commande système ? |
| Autorisation | Chaque route vérifie-t-elle le rôle et le propriétaire ? Le rôle vient-il du serveur ? |
| Sorties | Les données affichées sont-elles encodées ? Y a-t-il innerHTML, dangerouslySetInnerHTML, |raw, {!! !!} ? |
| Secrets | Un mot de passe, une clé, un secret JWT apparaît-il dans le code ou la configuration versionnée ? |
| Cryptographie | Algorithme des mots de passe ? Aléatoire cryptographique pour les jetons ? |
| Erreurs | Que se passe-t-il dans le catch ? Une erreur peut-elle laisser passer une requête ou révéler des détails ? |
| Journalisation | Les événements de sécurité sont-ils tracés ? Sans données sensibles ? |
| Dépendances | Une nouvelle dépendance est-elle justifiée, maintenue, à jour ? |
Un exemple de revue sur un extrait de la version PHP :
1 $id = $_GET['id'];
2 $res = $pdo->query("SELECT * FROM ligne_frais WHERE id_fiche = $id");
3 foreach ($res as $ligne) {
4 echo "<tr><td>" . $ligne['libelle'] . "</td><td>" . $ligne['montant'] . "</td></tr>";
5 }
Les commentaires de revue :
- Ligne 2 : injection SQL,
$idest concaténé. Requête préparée avec un marqueur. - Ligne 2 : aucune vérification que la fiche appartient à l'utilisateur connecté (IDOR). Ajouter
AND id_visiteur = ?via une jointure avecfiche_frais. - Ligne 2 :
SELECT *: ne sélectionner que les colonnes affichées. - Ligne 4 : XSS stockée possible,
libelleest affiché sans encodage.htmlspecialchars.
SAST : analyser le code source
Un outil SAST lit le code sans l'exécuter et cherche des motifs dangereux : une saisie de l'utilisateur qui atteint une requête SQL sans passer par une requête préparée, l'appel d'une fonction risquée, un secret écrit en dur. Semgrep est gratuit dans sa version communautaire et couvre de nombreux langages :
semgrep scan --config p/owasp-top-ten # règles publiques alignées sur le Top 10
semgrep scan --config p/javascript --error # code de sortie en erreur si un problème est trouvé
On peut aussi écrire ses propres règles, pour les erreurs propres au projet :
rules:
- id: requete-sql-concatenee
languages: [javascript]
severity: ERROR
message: Requête SQL construite par concaténation, utiliser une requête préparée
pattern-either:
- pattern: $POOL.query("..." + $X, ...)
- pattern: $POOL.execute("..." + $X, ...)
Les autres outils courants : SonarQube, qui combine qualité et sécurité et s'intègre aux pull requests ; CodeQL, le moteur de l'analyse de code de GitHub (code scanning), gratuit pour les dépôts publics ; eslint-plugin-security pour JavaScript ; Psalm et son analyse de propagation des données (taint analysis) pour PHP.
SCA et secrets dans la chaîne d'intégration
L'analyse des dépendances et la recherche de secrets se branchent au même endroit que les tests, pour bloquer une pull request qui ajoute une dépendance vulnérable ou un secret :
npm audit --omit=dev --audit-level=high # échoue s'il existe une vulnérabilité élevée ou critique
gitleaks git . --redact # cherche des secrets dans l'historique, sans les afficher en clair
Le chapitre 5 détaille npm audit et Dependabot ; le chapitre 4, la réaction à un secret commité.
DAST : attaquer l'application qui tourne
Un outil DAST se comporte comme un attaquant : il parcourt l'application en fonctionnement, envoie des requêtes piégées et analyse les réponses. Il ne voit pas le code, mais il voit ce que voit un attaquant : en-têtes, cookies, messages d'erreur, réactions aux charges d'injection.
ZAP (maintenu aujourd'hui sous le nom ZAP by Checkmarx) est le scanner libre le plus utilisé. Il fonctionne en trois temps :
- Proxy : ZAP se place entre le navigateur et l'application et enregistre toutes les requêtes pendant que tu utilises l'application normalement.
- Exploration (spider) : il suit les liens et les formulaires pour découvrir les pages et les routes.
- Analyse : l'analyse passive examine les réponses sans rien envoyer de plus (en-têtes manquants, cookies sans
HttpOnly, informations divulguées) ; l'analyse active envoie des charges d'attaque (injections, XSS, parcours de répertoire) et observe les réponses.
Pour une analyse rapide et automatisable, ZAP fournit des scripts prêts à l'emploi dans son image Docker :
# Analyse de base : exploration et analyse passive, sans attaque, rapport HTML dans le dossier courant
docker run --rm -v "$(pwd):/zap/wrk" -t ghcr.io/zaproxy/zaproxy:stable \
zap-baseline.py -t https://preprod.frais.gsb.fr -r rapport-zap.html
Le script zap-full-scan.py ajoute l'analyse active, et zap-api-scan.py analyse une API à partir de sa description OpenAPI.
Burp Suite, dont l'édition Community est gratuite, est l'outil de référence des testeurs pour intercepter, modifier et rejouer les requêtes à la main : changer un identifiant pour tester une IDOR, retirer un jeton, modifier un champ caché.
Tester l'autorisation avec des tests automatisés
Aucun scanner ne sait que le visiteur A ne doit pas lire la fiche du visiteur B : c'est une règle métier. Elle se protège par des tests écrits par l'équipe, exécutés à chaque commit. Une approche efficace : une matrice de tests qui joue chaque rôle contre chaque route sensible.
import { describe, it, expect } from 'vitest';
import request from 'supertest';
import { app } from '../src/app.js';
import { jetonPour } from './outils.js';
const cas = [
// [méthode, route, utilisateur, statut attendu]
['get', '/api/fiches/1187', 'visiteurA', 200],
['get', '/api/fiches/1188', 'visiteurA', 404],
['get', '/api/fiches/1188', 'comptable1', 200],
['patch', '/api/compta/fiches/1187/valider', 'visiteurA', 403],
['patch', '/api/compta/fiches/1187/valider', null, 401],
['get', '/api/moi', 'visiteurA', 200],
];
describe('matrice de contrôle d’accès', () => {
it.each(cas)('%s %s par %s → %i', async (methode, route, utilisateur, attendu) => {
let requete = request(app)[methode](route);
if (utilisateur) requete = requete.set('Authorization', `Bearer ${jetonPour(utilisateur)}`);
const reponse = await requete;
expect(reponse.status).toBe(attendu);
});
});
Un test qui vérifie aussi qu'une réponse ne contient pas de champ sensible (expect(reponse.body).not.toHaveProperty('mdp_hash')) protège contre le BOPLA. Le cours Tests présente les tests unitaires et d'intégration.
Intégrer les tests dans la chaîne d'intégration continue
Avec GitHub Actions, un fichier .github/workflows/securite.yml exécute les contrôles à chaque pull request :
name: securite
on: [pull_request]
jobs:
controles:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # tout l'historique, pour la recherche de secrets
- uses: actions/setup-node@v4
with:
node-version: 24
- run: npm ci
- run: npm test # dont la matrice de contrôle d'accès
- run: npm audit --omit=dev --audit-level=high
- uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: pipx run semgrep scan --config p/owasp-top-ten --error
Une règle de protection de la branche principale exige que ce workflow réussisse et qu'un relecteur approuve avant toute fusion. Le scan ZAP se lance après le déploiement en préproduction, dans un autre workflow. Le chapitre Sécurité dans la CI : SAST, dépendances et secrets du cours CI/CD détaille cette démarche DevSecOps, avec ses équivalents dans GitLab CI.
Le test d'intrusion et le rapport d'audit
Un test d'intrusion (pentest) est une attaque simulée menée par un expert, avec l'accord écrit de l'organisation. La convention fixe le périmètre (quelles applications, quelles adresses), les dates, les techniques autorisées ou interdites (pas de déni de service, pas d'ingénierie sociale…) et les contacts en cas de problème. Sans elle, le test est une intrusion au sens du Code pénal (chapitre 1). On distingue le test en boîte noire (le testeur ne sait rien), en boîte grise (il a un compte utilisateur) et en boîte blanche (il a le code). En France, l'ANSSI qualifie des prestataires d'audit (label PASSI) ; des plateformes de bug bounty rémunèrent les chercheurs qui signalent des failles dans un cadre défini.
Un audit, qu'il soit mené par toi sur ton projet ou par un prestataire, se termine par un rapport. Sa structure :
- Synthèse pour la direction : périmètre, dates, niveau de sécurité global, les failles les plus graves en langage non technique, les priorités.
- Méthodologie : outils, référentiel suivi (OWASP Top 10, ASVS, WSTG), limites du test.
- Une fiche par vulnérabilité :
| Rubrique | Exemple |
|---|---|
| Titre | Accès aux fiches de frais des autres visiteurs (IDOR) |
| Catégorie | A01:2025 Contrôle d'accès défaillant, CWE-639 |
| Gravité | Moyenne selon CVSS 3.1 (6,5, AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N), relevée à élevée dans le contexte : les RIB et les déplacements de tous les visiteurs sont exposés, et c'est une violation de données à notifier |
| Description | La route GET /api/fiches/:id renvoie toute fiche existante à un utilisateur authentifié, sans vérifier qu'il en est le propriétaire |
| Preuve | Requête avec le jeton du visiteur A sur la fiche 1188 du visiteur B, réponse 200 avec les données de B |
| Impact | Lecture des montants, déplacements et RIB de tous les visiteurs : violation de données personnelles |
| Recommandation | Filtrer par id_visiteur dans la requête ; ajouter un test automatisé ; journaliser les refus |
| Statut | Corrigée le …, vérifiée le … |
- Plan d'action : les corrections classées par priorité (gravité, facilité d'exploitation, coût de correction), avec un responsable et une date.
Après correction, on vérifie : on rejoue la preuve, qui doit échouer, et on garde le test automatisé qui empêche le retour de la faille.
Exercice 1 sur 3 Quelle famille de test ? Réussi À revoir
Pour chaque faille, indique la famille de test la plus susceptible de la trouver en premier.
- Le projet utilise une version de
jsonwebtokentouchée par une CVE. - La réponse du serveur ne contient pas d'en-tête
Strict-Transport-Security. - Un fichier
config.jscontient une clé d'API d'un service de paiement. - Un comptable peut valider une fiche dont il est lui-même le visiteur.
- Une requête SQL est construite par concaténation dans un contrôleur.
Voir la réponseMasquer la réponse de l'exercice 1
- SCA (
npm audit, Dependabot). - DAST (ZAP le signale dès l'analyse passive) ; la revue de la configuration peut aussi le voir.
- Recherche de secrets (gitleaks, GitHub secret scanning), et SAST.
- Revue de code ou test automatisé écrit à partir de la règle métier : aucun outil générique ne connaît cette règle. Un test d'intrusion en boîte grise peut aussi la trouver.
- SAST (Semgrep, SonarQube) et revue de code.
Tu avais trouvé ?
Exercice 2 sur 3 Faire une revue de code Réussi À revoir
Fais la revue de cette route Express : relève chaque problème, sa catégorie OWASP 2025 et la correction.
router.get('/api/recherche', async (req, res) => {
try {
const sql = "SELECT * FROM ligne_frais WHERE libelle LIKE '%" + req.query.q + "%'";
const [lignes] = await pool.query(sql);
res.send(`<h2>Résultats pour ${req.query.q}</h2>` + lignes.map(l => `<p>${l.libelle}</p>`).join(''));
} catch (e) {
res.status(500).send(e.stack);
}
});
Voir la réponseMasquer la réponse de l'exercice 2
- Pas d'authentification et pas de filtre sur le propriétaire : n'importe qui cherche dans les lignes de tous les visiteurs (A01). Ajouter
authentifieret une jointure surfiche_fraisavecid_visiteur = ?. - Injection SQL :
qest concaténé (A05). Requête préparée :WHERE libelle LIKE ?avec la valeur`%${q}%`. - XSS réfléchie (
req.query.qdans le titre) et XSS stockée (l.libelle) : aucune donnée n'est encodée (A05). Encoder, ou mieux, renvoyer du JSON et laisser le front (React) afficher le texte. SELECT *: ne sélectionner que les colonnes utiles.- Trace d'erreur envoyée au client (
e.stack) : divulgation d'informations (A02, A10). Message générique, détail dans le journal. - Pas de pagination : une recherche vide renvoie tout (consommation de ressources).
Tu avais trouvé ?
Exercice 3 sur 3 Rédiger une fiche de vulnérabilité Réussi À revoir
Pendant un scan, ZAP signale que le cookie de session PHPSESSID n'a pas l'attribut HttpOnly. Rédige la fiche de vulnérabilité (titre, catégorie, gravité qualitative, description, impact, recommandation).
Voir la réponseMasquer la réponse de l'exercice 3
- Titre : cookie de session lisible par JavaScript.
- Catégorie : A02:2025 Mauvaise configuration de sécurité (CWE-1004).
- Gravité : moyenne. Seule, elle n'est pas exploitable ; combinée à une XSS, elle permet le vol de session.
- Description : le serveur envoie
Set-Cookie: PHPSESSID=…; path=/sans l'attributHttpOnly(niSameSite). - Impact : un script injecté par une XSS peut lire le cookie avec
document.cookie, l'envoyer à l'attaquant, qui usurpe alors la session de la victime. - Recommandation :
session_set_cookie_params(['secure' => true, 'httponly' => true, 'samesite' => 'Lax'])avantsession_start()(ou les directivessession.cookie_httponly,session.cookie_secureetsession.cookie_samesitedephp.ini) ; vérifier ensuite l'en-tête aveccurl -I.
Tu avais trouvé ?