Le DAST (Dynamic Application Security Testing) attaque une application en cours d'exécution, comme le ferait un attaquant, pour trouver les failles réellement exploitables. Cette leçon s'adresse à un développeur ou à un DevOps qui a déjà vu le SAST et veut savoir ce qu'un scan dynamique apporte, et ce qu'il rate. Tout repose sur une mesure : la même petite application Flask que la leçon SAST, avec une injection SQL et une injection de commande. Un scan OWASP ZAP lancé avec ses réglages par défaut rend 7 avertissements, tous sur des en-têtes HTTP, et ne voit aucune des deux injections. La leçon montre pourquoi, et comment obtenir un scan qui les trouve et fait échouer le pipeline.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Distinguer ce que le DAST détecte de ce que le SAST voit déjà
- Lancer les trois scans de ZAP : baseline, complet et d'API
- Mesurer la couverture d'un scan avant de croire son rapport
- Faire échouer un job CI sur les seules failles décidées, avec un fichier de règles
- Situer Nuclei, un moteur à templates, par rapport à ZAP
- Brancher le scan après le déploiement en staging, dans le cadre légal
Qu'est-ce que le DAST ?
Section intitulée « Qu'est-ce que le DAST ? »Le DAST teste une application déployée en lui envoyant des requêtes malformées et en analysant ses réponses. C'est un test en boîte noire : le scanner ne lit pas le code source, il interagit avec l'application par le réseau, comme un utilisateur ou un attaquant. Il répond à une question simple : « mon application, telle qu'elle tourne, est-elle vulnérable à des attaques connues ? ».
Un scan dynamique enchaîne toujours les mêmes phases, et la première décide de tout le reste :
-
Exploration (crawling) : le scanner parcourt l'application pour découvrir pages, formulaires et endpoints. Ce qu'il ne découvre pas, il ne l'attaque pas.
-
Analyse passive : il lit les réponses obtenues sans rien modifier, en-têtes de sécurité, cookies, fuites d'informations.
-
Analyse active : sur chaque point d'entrée découvert, il injecte des charges d'attaque (SQL, commande système, XSS) et observe la réponse.
-
Rapport : chaque constat sort avec une sévérité, la requête qui l'a déclenché et la réponse obtenue.
DAST et SAST : ce que chacun voit
Section intitulée « DAST et SAST : ce que chacun voit »Les deux familles n'observent pas le même objet. Le SAST lit du texte
source et voit tout le code, y compris les branches jamais exécutées. Le
DAST observe un processus qui répond sur le réseau et ne voit que ce que son
exploration a atteint. Sur l'application de test, le SAST a trouvé les deux
injections en lisant app.py ; le DAST, on va le voir, ne les trouve que si on
l'amène jusqu'à elles.
| Aspect | DAST | SAST |
|---|---|---|
| Quand | Application déployée | Pendant le développement |
| Comment | Requêtes réseau, boîte noire | Lecture du code source |
| Trouve | Failles observées en réponse | Failles potentielles dans le code |
| Couverture | Chemins atteints par l'exploration | Tout le code |
| Voit aussi | Configuration réelle : en-têtes, TLS, erreurs | Rien de ce qui n'est pas dans le dépôt |
Ce que le SAST ne peut pas voir tient dans la dernière ligne : un en-tête de sécurité absent, une page d'erreur bavarde ou un TLS faible n'existent pas dans le code, ils naissent au déploiement. La preuve qu'il apporte est aussi plus parlante : la requête et la réponse sont jointes au constat, ce qui coupe court au débat « est-ce vraiment exploitable ? ».
Le banc de mesure
Section intitulée « Le banc de mesure »Toutes les sorties de cette leçon viennent d'un banc local : l'application
Flask de la leçon SAST, conteneurisée et lancée dans un réseau Docker dédié,
nommé dast-net, sous le nom cible. Elle expose deux routes vulnérables,
/user?id= (injection SQL dans SQLite) et /ping?host= (injection de commande
via shell=True), et une page d'accueil qui ne contient aucun lien vers
elles, comme une API qui n'a pas d'interface web.
Les deux scanners tournent dans leur image officielle, épinglée par digest :
ZAP="ghcr.io/zaproxy/zaproxy:2.17.0@sha256:781a2bdaea47324e7bab583e2263f21d257b0aee61ed51521a5be45f5f5081ef"NUCLEI="projectdiscovery/nuclei:v3.11.1@sha256:582d5546902e67052097cb2d07296c642d50a1afc5e44623cb038845df9a32eb"Avant le premier scan, vérifiez à la main que la faille existe : c'est elle
que le scanner doit retrouver. /user?id=1 renvoie [(1, 'alice')], et
/user?id=1 OR 1=1 renvoie les deux utilisateurs de la table.
Que trouve un premier scan ZAP ?
Section intitulée « Que trouve un premier scan ZAP ? »OWASP ZAP (Zed Attack Proxy) est un scanner DAST open source, utilisé ici
comme scanner de démonstration. Son image fournit trois scripts prêts à
l'emploi, du plus léger au plus complet. Le premier, zap-baseline.py, explore l'application puis se contente
de l'analyse passive : il n'envoie aucune charge d'attaque, ce qui le rend
sûr sur n'importe quel environnement.
mkdir -p wrk && chmod 777 wrk # le conteneur écrit sous l'utilisateur zapdocker run --rm --network dast-net -v "$PWD/wrk:/zap/wrk:rw" "$ZAP" \ zap-baseline.py -t http://cible:5000 -r baseline.html -J baseline.jsonLe montage de /zap/wrk est obligatoire pour récupérer les rapports : les
options -r et -J y écrivent, jamais dans le dossier courant. Mesuré : le
scan dure 36 secondes, découvre 3 URL (/, robots.txt et
sitemap.xml), passe 60 règles et lève 7 avertissements, tous sur des
en-têtes HTTP :
WARN-NEW: Missing Anti-clickjacking Header [10020] x 1WARN-NEW: X-Content-Type-Options Header Missing [10021] x 1WARN-NEW: Server Leaks Version Information via "Server" HTTP Response Header Field [10036] x 3WARN-NEW: Content Security Policy (CSP) Header Not Set [10038] x 3WARN-NEW: Storable and Cacheable Content [10049] x 2WARN-NEW: Permissions Policy Header Not Set [10063] x 3WARN-NEW: Cross-Origin-Embedder-Policy Header Missing or Invalid [90004] x 3FAIL-NEW: 0 FAIL-INPROG: 0 WARN-NEW: 7 WARN-INPROG: 0 INFO: 0 IGNORE: 0 PASS: 60Ces avertissements sont réels et utiles : ils décrivent la configuration de
déploiement. Mais les deux injections n'apparaissent nulle part, et c'est
normal : le baseline n'attaque pas, et son exploration n'a jamais atteint
/user ni /ping.
Le scan complet ne voit pas mieux
Section intitulée « Le scan complet ne voit pas mieux »Le deuxième script, zap-full-scan.py, ajoute l'analyse active : il
injecte des charges d'attaque dans chaque point d'entrée découvert. Il est plus
long et plus intrusif, à réserver à un environnement de test.
docker run --rm --network dast-net -v "$PWD/wrk:/zap/wrk:rw" "$ZAP" \ zap-full-scan.py -t http://cible:5000 -r full.html -J full.jsonMesuré : 72 secondes, 4 URL découvertes, 135 règles passées sans lever la moindre injection. Le scan actif a attaqué avec soin ce qu'il avait sous la main, c'est-à-dire la page d'accueil. Un rapport vert sur ce périmètre ne dit rien de l'application.
Pourquoi la couverture décide de tout
Section intitulée « Pourquoi la couverture décide de tout »Un scanner dynamique n'attaque que ce qu'il connaît. Une API sans interface, des routes atteintes seulement par du JavaScript ou une zone derrière un formulaire de connexion restent invisibles à l'exploration, et le rapport paraît rassurant à tort. Le remède consiste à donner la carte au scanner : une description OpenAPI pour une API, un contexte authentifié pour une application web.
Le troisième script, zap-api-scan.py, lit une description d'API au lieu
d'explorer. Pour l'application de test, la description tient en quelques lignes :
chaque route, son paramètre et une valeur d'exemple.
openapi: 3.0.3info: title: Démo version: 1.0.0servers: - url: http://cible:5000paths: /user: get: parameters: - name: id in: query required: true schema: type: string example: "1" responses: "200": description: Utilisateur /ping: get: parameters: - name: host in: query required: true schema: type: string example: localhost responses: "200": description: Résultat du pingdocker run --rm --network dast-net -v "$PWD/wrk:/zap/wrk:rw" "$ZAP" \ zap-api-scan.py -t /zap/wrk/openapi.yaml -f openapi -r api.html -J api.jsonMesuré : 39 secondes, 32 URL attaquées, et les deux failles sortent, chacune avec la requête qui la prouve :
WARN-NEW: SQL Injection [40018] x 1 http://cible:5000/user?id=3-2 (200 OK)WARN-NEW: Remote OS Command Injection [90020] x 1 http://cible:5000/ping?host=localhost%26cat+%2Fetc%2Fpasswd%26 (200 OK)FAIL-NEW: 0 FAIL-INPROG: 0 WARN-NEW: 11 WARN-INPROG: 0 INFO: 0 IGNORE: 0 PASS: 109La preuve de l'injection SQL est élégante : id=3-2 renvoie le même
utilisateur que id=1, donc la base a calculé l'expression, donc la valeur
entre dans la requête sans être traitée comme une donnée. Le rapport contient
aussi du bruit : un « Buffer Overflow » sur une application Python, levé
parce qu'une charge trop longue a provoqué une erreur 500, et 17
« Unexpected Content-Type ». Un constat DAST se rejoue à la main avant
d'être corrigé ou écarté.
Comment faire échouer le pipeline ?
Section intitulée « Comment faire échouer le pipeline ? »Regardez la dernière ligne du scan d'API : FAIL-NEW: 0. Une injection SQL
confirmée sort en WARN, pas en FAIL : par défaut, les scripts de ZAP rangent
toutes les règles au niveau avertissement. Le code de sortie dépend de
ce classement :
| Code | Signification |
|---|---|
0 | Aucun avertissement ni échec |
1 | Au moins une règle au niveau FAIL a levé un constat |
2 | Au moins un avertissement, aucun échec |
3 | Erreur d'exécution du scan |
Les trois scans précédents sont tous sortis en code 2. Un job qui échoue sur tout code non nul bloque donc sur un en-tête manquant autant que sur une injection, et l'équipe finit par désactiver le contrôle. La parade tient en un fichier de règles, une ligne par règle, séparée par des tabulations : l'identifiant, le niveau voulu, et un nom pour le lecteur.
40018 FAIL (SQL Injection)90020 FAIL (Remote OS Command Injection)10036 IGNORE (Server Leaks Version Information)L'option -c charge ce fichier, et -I empêche un simple avertissement de
faire échouer le scan. Combinées, elles donnent exactement la politique voulue :
docker run --rm --network dast-net -v "$PWD/wrk:/zap/wrk:rw" "$ZAP" \ zap-api-scan.py -t /zap/wrk/openapi.yaml -f openapi -c rules.tsv -I -sIGNORE-NEW: Server Leaks Version Information via "Server" HTTP Response Header Field [10036] x 2FAIL-NEW: SQL Injection [40018] x 1FAIL-NEW: Remote OS Command Injection [90020] x 1FAIL-NEW: 2 FAIL-INPROG: 0 WARN-NEW: 9 WARN-INPROG: 0 INFO: 0 IGNORE: 1 PASS: 108Mesuré : ce scan sort en code 1, et le baseline relancé avec -I seul sort
en code 0 malgré ses 7 avertissements. Les deux injections bloquent, les
en-têtes restent visibles dans le rapport sans arrêter la livraison. Le fichier
complet des règles se génère avec -g gen.conf : partez de lui, et ne passez
une règle en IGNORE qu'avec une justification écrite en revue.
Nuclei : des templates, pas un crawler
Section intitulée « Nuclei : des templates, pas un crawler »Nuclei, de ProjectDiscovery, fonctionne autrement : il n'explore pas l'application, il exécute des templates YAML, chacun décrivant une requête et la réponse qui trahit une faille connue. Sa force est le volume : le dépôt de templates mesuré compte 13 742 fichiers, dont 251 dédiés au fuzzing DAST. Sa faiblesse est symétrique : il cherche ce que décrivent ses templates, surtout des CVE de produits connus et des expositions de configuration.
docker volume create nuclei-templatesN=(docker run --rm --network dast-net -v nuclei-templates:/root/nuclei-templates -v "$PWD:/wrk" "$NUCLEI")
"${N[@]}" -u http://cible:5000 # tous les templates"${N[@]}" -u http://cible:5000 -s critical,high # sévérités hautes seulementMesuré sur l'application de test, le scan complet dure 378 secondes et
lève 12 constats, tous de sévérité info : en-têtes manquants, détection de
Python, méthode OPTIONS. Filtré sur critical,high, il dure 86 secondes et ne
trouve rien. C'est attendu : l'application n'utilise aucun produit
vulnérable connu, et ses failles sont propres à son code.
Le mode -dast active les templates de fuzzing, à appliquer à une liste d'URL
avec paramètres, car Nuclei ne les découvre pas seul :
printf 'http://cible:5000/user?id=1\nhttp://cible:5000/ping?host=localhost\n' > urls.txt"${N[@]}" -l /wrk/urls.txt -dast # le dossier courant est monté sur /wrkMesuré, en trois variantes qui ne disent pas la même chose :
| Variante | Résultat | Remarque |
|---|---|---|
-dast | injection de commande trouvée | par une requête de rappel vers oast.me |
-dast -ni | rien | sans serveur de rappel externe |
-dast -ni -fa high | injection de commande trouvée | étiquetée linux-lfi-fuzz, lecture de fichier |
L'injection SQL n'est sortie dans aucune variante. Et la première ligne
cache un point de vigilance : les templates OAST (Out-of-band Application
Security Testing) font appeler par la cible un serveur public, oast.me ici,
pour prouver l'exécution. Le domaine de rappel transite donc par un service
tiers. Sur un environnement sensible, désactivez-les avec -ni ou hébergez
votre propre serveur interactsh et passez-le avec -iserver.
Dernier point mesuré : Nuclei sort en code 0 même quand il trouve. Pour
bloquer un pipeline, il faut tester le fichier de résultats produit par -o,
pas le code de sortie. Le guide Nuclei
détaille les templates et leur écriture.
Où brancher le DAST dans le pipeline ?
Section intitulée « Où brancher le DAST dans le pipeline ? »Le scan dynamique a besoin d'une application qui tourne : il se place juste après le déploiement en staging, sur un environnement configuré comme la production, et jamais directement sur la production, qu'un scan actif peut perturber (données créées, files saturées, alertes déclenchées).
build → tests → déploiement staging → scan DAST → déploiement productionLe déclencheur compte autant que la position : le job attend la fin du déploiement, pas le simple push, sinon il teste l'ancienne version encore en ligne.
name: DAST
on: workflow_run: workflows: ["Deploy to Staging"] types: [completed]
permissions: {}
jobs: zap-api-scan: if: ${{ github.event.workflow_run.conclusion == 'success' }} runs-on: ubuntu-24.04 permissions: contents: read steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false
- name: ZAP API scan uses: zaproxy/action-api-scan@5158fe4d9d8fcc75ea204db81317cce7f9e5453d # v0.10.0 with: target: https://staging.mon-app.example.com/openapi.yaml format: openapi docker_name: ghcr.io/zaproxy/zaproxy:2.17.0@sha256:781a2bdaea47324e7bab583e2263f21d257b0aee61ed51521a5be45f5f5081ef rules_file_name: .zap/rules.tsv cmd_options: -I fail_action: true allow_issue_writing: falseTrois entrées de l'action se règlent explicitement. docker_name vaut par
défaut ghcr.io/zaproxy/zaproxy:stable, une référence mutable : on la
remplace par l'image épinglée. fail_action: true fait échouer le job sur les
codes 1 et 2 (lu dans le code de l'action), d'où -I dans cmd_options
pour ne bloquer que sur les règles FAIL. Enfin, l'action ouvre par défaut une
issue récapitulative, ce qui exigerait issues: write : avec
allow_issue_writing: false, le job se contente de contents: read.
dast: stage: dast image: ghcr.io/zaproxy/zaproxy:2.17.0@sha256:781a2bdaea47324e7bab583e2263f21d257b0aee61ed51521a5be45f5f5081ef needs: - deploy_staging script: - mkdir -p /zap/wrk - cp .zap/rules.tsv /zap/wrk/rules.tsv - zap-api-scan.py -t "$CI_ENVIRONMENT_URL/openapi.yaml" -f openapi -c rules.tsv -I -r zap-report.html after_script: - cp /zap/wrk/zap-report.html "$CI_PROJECT_DIR/zap-report.html" artifacts: when: always paths: - zap-report.htmlLe détour par /zap/wrk n'est pas une coquetterie : les scripts de ZAP y
lisent le fichier de règles et y écrivent leurs rapports. Sans la copie vers
$CI_PROJECT_DIR, l'artefact remonte vide. Le after_script récupère le
rapport même quand le scan échoue, c'est-à-dire précisément quand vous en
avez besoin.
Préparer l'environnement et respecter le cadre légal
Section intitulée « Préparer l'environnement et respecter le cadre légal »Un scan actif envoie des milliers de requêtes anormales. Sans préparation, il déclenche le WAF, sature le rate limiting et réveille l'astreinte pour une fausse attaque. Quatre réglages se font une fois pour toutes, avant le premier scan :
- isoler l'environnement de staging, sans données de production ;
- autoriser l'adresse du scanner dans le WAF et le pare-feu ;
- exclure les endpoints destructifs (déconnexion, suppression) du périmètre ;
- prévenir l'équipe d'exploitation de la fenêtre de scan.
Le cadre légal ne se règle pas, il se respecte. Contrairement au SAST, qui lit un dépôt dont vous êtes propriétaire, le DAST attaque un système joignable sur le réseau. Le fait qu'il appartienne à votre employeur ne suffit pas : c'est l'autorisation écrite du responsable du système qui vous protège, et elle nomme les URL et la fenêtre de tir.
Quelles sont les limites du DAST ?
Section intitulée « Quelles sont les limites du DAST ? »La limite la plus dangereuse est la couverture partielle, et le banc l'a
montrée sans détour : un rapport vert signifie « rien trouvé sur les chemins
atteints », jamais « application saine ». Relevez systématiquement le nombre
d'URL découvertes (Total of N URLs en tête de sortie) et comparez-le à ce
que vous savez de l'application : 3 URL pour une API à deux routes invalident le
rapport avant même de lire les alertes.
Le DAST arrive aussi tard dans le cycle, quand la faille est déjà déployée, et il demande un environnement qui tourne. Il voit mal la logique métier : un scanner constate qu'une ressource répond, il ne sait pas si l'identifiant demandé appartient au compte connecté. C'est pourquoi il complète le SAST et la SCA, sans les remplacer : chacun voit ce que les deux autres ne voient pas.
À retenir
Section intitulée « À retenir »- Le DAST attaque l'application déployée, en boîte noire, et voit ce que le code ne contient pas : en-têtes, erreurs, TLS.
- Un scan n'attaque que ce que son exploration a découvert : mesuré, 3 URL et aucune injection sans aide.
- Une description OpenAPI donne la carte au scanner : 32 URL attaquées, les deux injections trouvées.
- Par défaut, ZAP range tout en WARN et sort en code 2 : un fichier de règles et
-Iréservent l'échec aux failles décidées. - Nuclei excelle sur les CVE de produits connus ; ses templates OAST font appeler un serveur tiers, et il sort en code 0 même quand il trouve.
- Le scan se branche après le déploiement en staging, avec une image épinglée ; hors de vos propres systèmes, la règle professionnelle exige une autorisation écrite.
Contrôle de connaissances
Section intitulée « Contrôle de connaissances »Vérifiez que l'essentiel de cette leçon est acquis. Les questions portent uniquement sur ce qui vient d'être expliqué ici.
Contrôle de connaissances
Validez vos connaissances avec ce quiz interactif
Informations
- Le chronomètre démarre au clic sur Démarrer
- Questions à choix multiples, vrai/faux et réponses courtes
- Vous pouvez naviguer entre les questions
- Les résultats détaillés sont affichés à la fin
Lance le quiz et démarre le chronomètre
Vérification
(0/0)Score par domaine
Un domaine regroupe plusieurs guides et reçoit plusieurs questions : son score est plus fiable qu'un score par thème, qui porte souvent sur une seule question.
Bilan
Points forts
Aucun sujet réussi à 80 % sur au moins deux questions pour l’instant.
Points faibles
Aucun sujet sous le seuil de réussite.
À faire ensuite
Des indices pour retenter votre chance ?
Continuer le cours : Pourquoi les secrets statiques posent problème
Nouveau quiz complet avec des questions aléatoires
Retravailler uniquement les questions ratées
Retour à la liste des certifications