Aller au contenu
English
Compte : non connecté, synchronisation possible Mon apprentissage
Selectionner le thème
English

DAST : tester la sécurité d'une application en fonctionnement

Par mis à jour le

Améliorer cette page

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.

  • 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

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 :

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

  2. Analyse passive : il lit les réponses obtenues sans rien modifier, en-têtes de sécurité, cookies, fuites d'informations.

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

  4. Rapport : chaque constat sort avec une sévérité, la requête qui l'a déclenché et la réponse obtenue.

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.

AspectDASTSAST
QuandApplication déployéePendant le développement
CommentRequêtes réseau, boîte noireLecture du code source
TrouveFailles observées en réponseFailles potentielles dans le code
CouvertureChemins atteints par l'explorationTout le code
Voit aussiConfiguration réelle : en-têtes, TLS, erreursRien 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 ? ».

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 :

Fenêtre de terminal
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.

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.

Fenêtre de terminal
mkdir -p wrk && chmod 777 wrk # le conteneur écrit sous l'utilisateur zap
docker run --rm --network dast-net -v "$PWD/wrk:/zap/wrk:rw" "$ZAP" \
zap-baseline.py -t http://cible:5000 -r baseline.html -J baseline.json

Le 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 1
WARN-NEW: X-Content-Type-Options Header Missing [10021] x 1
WARN-NEW: Server Leaks Version Information via "Server" HTTP Response Header Field [10036] x 3
WARN-NEW: Content Security Policy (CSP) Header Not Set [10038] x 3
WARN-NEW: Storable and Cacheable Content [10049] x 2
WARN-NEW: Permissions Policy Header Not Set [10063] x 3
WARN-NEW: Cross-Origin-Embedder-Policy Header Missing or Invalid [90004] x 3
FAIL-NEW: 0 FAIL-INPROG: 0 WARN-NEW: 7 WARN-INPROG: 0 INFO: 0 IGNORE: 0 PASS: 60

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

Fenêtre de terminal
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.json

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

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.

Couverture d'un scan DAST sur l'application de test : avec son crawler seul, ZAP découvre 3 URL et son rapport ne contient que des en-têtes HTTP, sans injection ; à partir d'une description OpenAPI, le scan actif attaque 32 URL et trouve l'injection SQL et l'injection de commande.Applicationdéployée en stagingCrawler seul :3 URL découvertesDescriptionOpenAPIScan actif :32 URL attaquéesRapport : en-têtesHTTP, aucune injectionInjection SQL etinjection de commande
Source · SVG
Échap pour fermer, + et − pour zoomer, défilement pour se déplacer. 100 %

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.

wrk/openapi.yaml
openapi: 3.0.3
info:
title: Démo
version: 1.0.0
servers:
- url: http://cible:5000
paths:
/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 ping
Fenêtre de terminal
docker 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.json

Mesuré : 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: 109

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

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 :

CodeSignification
0Aucun avertissement ni échec
1Au moins une règle au niveau FAIL a levé un constat
2Au moins un avertissement, aucun échec
3Erreur 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.

wrk/rules.tsv
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 :

Fenêtre de terminal
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 -s
IGNORE-NEW: Server Leaks Version Information via "Server" HTTP Response Header Field [10036] x 2
FAIL-NEW: SQL Injection [40018] x 1
FAIL-NEW: Remote OS Command Injection [90020] x 1
FAIL-NEW: 2 FAIL-INPROG: 0 WARN-NEW: 9 WARN-INPROG: 0 INFO: 0 IGNORE: 1 PASS: 108

Mesuré : 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, 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.

Fenêtre de terminal
docker volume create nuclei-templates
N=(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 seulement

Mesuré 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 :

Fenêtre de terminal
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 /wrk

Mesuré, en trois variantes qui ne disent pas la même chose :

VarianteRésultatRemarque
-dastinjection de commande trouvéepar une requête de rappel vers oast.me
-dast -niriensans serveur de rappel externe
-dast -ni -fa highinjection 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.

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 production

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

.github/workflows/dast.yml
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: false

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

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.

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.

  • 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 -I ré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.

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

6 questions
6 min.
70% requis

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

Ce projet vous est utile ?

Les guides, formations, quiz et labs restent gratuits. Si vous les utilisez régulièrement, vous pouvez contribuer à leur maintenance et à leur développement.

Devenir membre de soutien (nouvel onglet) Soutenir une fois (nouvel onglet) En savoir plus

Suivez aussi le projet sur LinkedIn