Blog

OWASP Top 10, CWE, CVE, ASVS : comprendre la logique qui les relie

Des centaines de pages de définitions, quatre sigles qui se ressemblent, une littérature austère qui décourage dès les premières lignes. On referme l'onglet, et la sécurité attend encore un peu.

Derrière cette façade se cache pourtant un vrai savoir d'expert, et des piliers devenus vitaux pour bâtir un logiciel robuste. Nous vous proposons d'y entrer par la porte la plus utile : ce que chacun de ces référentiels répond vraiment, comment ils se relient, et avec quels outils on les met en pratique.

Nicolas Bastien Nicolas Bastien
|
|
31 min de lecture
| Tech
Résumez cette page avec votre IA préférée :
OWASP Top 10, CWE, CVE, ASVS : le fil qui relie les référentiels de sécurité

Introduction

Les articles sur l’OWASP Top 10 racontent tous la même chose : ils reprennent les dix familles, les traduisent, ajoutent un exemple par ligne, et concluent qu’il faut « faire attention ». À la fin de la lecture, on sait que le contrôle d’accès défaillant est le risque numéro un. On ne sait toujours pas quoi en faire au quotidien.

Le problème est ailleurs, et il est très concret : aucun de vos outils ne parle le langage du Top 10. Psalm vous dira TaintedSql. ZAP vous dira cweid=79. composer audit vous dira CVE-2024-50345. Pas un seul ne vous mentionnera directement un point de la liste du Top 10 comme « A05:2025 Injection ». Le lien entre ce que vous lisez dans un rapport et ce que vous racontez à votre client n’existe nulle part dans la documentation.

Ce lien existe pourtant, et il passe par deux référentiels dont on parle beaucoup moins : les CWE et l’ASVS. Cet article explique d’abord comment les quatre s’articulent, puis déroule un cas réel de bout en bout sur notre chaîne d’outils : là où elle attrape le défaut toute seule, et là où elle ne voit rien du tout.

En résumé, pour extraire l'essentiel

L’OWASP Top 10 dit où regarder en premier. L’ASVS dit ce qui doit être vrai. Le CWE dit de quel défaut on parle. La CVE dit quelle faille précise, dans quel produit. Ce sont quatre questions différentes, pas quatre niveaux de détail du même document. Le CWE est le pivot : c’est le seul identifiant que vos outils émettent et que les deux référentiels OWASP citent.


Partie 1 : la théorie

1. Quatre référentiels, quatre questions différentes

L’OWASP Top 10 : où porter l’effort en premier

Publié par l’OWASP Foundation, le Top 10 classe dix familles de risques par fréquence observée en production. L’édition 2025 s’appuie sur l’analyse de millions d’applications : huit catégories sont déduites directement des données collectées, deux viennent d’un vote de la communauté pour anticiper ce que la donnée ne capte pas encore.

Le point que presque personne ne mentionne : le Top 10 est fabriqué à partir des CWE. La méthodologie mesure un taux d’incidence par type de faiblesse, puis regroupe ces types en dix groupes. La catégorie A01:2025 Broken Access Control, à elle seule, rassemble une quarantaine de CWE distincts.

C’est un document de sensibilisation et de priorisation. Il ne dit ni quoi corriger, ni comment prouver que c’est corrigé.

Le CWE : de quel défaut parle-t-on exactement

CWE (Common Weakness Enumeration) n’est pas un projet OWASP. C’est le MITRE qui le publie, sur financement de l’agence américaine CISA. C’est la confusion la plus fréquente qui empêche de comprendre le reste.

Le CWE est le catalogue des types de faiblesse : environ 940 entrées, organisées de la famille très générale au cas très précis. Là où le Top 10 met tout dans un même sac, le CWE distingue des défauts qui n’ont pas du tout le même correctif :

CWECe que ça décritCe qu’il faut faire
CWE-862Missing Authorization : aucun contrôle n’existe sur la routeAjouter un contrôle
CWE-863Incorrect Authorization : le contrôle existe mais se trompeCorriger la règle
CWE-639Le contrôle porte sur une clé fournie par l’utilisateurVérifier la propriété de l’objet

Ces lignes sont toutes « A01 ». Elles appellent des correctifs différents. Voilà pourquoi rester au niveau de la catégorie ne permet aucune action.

En contrepartie, le CWE ne prescrit rien. Il décrit ce qui ne va pas. Il ne dit pas ce qui doit être fait.

La CVE : quelle faille réelle, dans quel produit

Même éditeur, sujet différent. Une CVE (Common Vulnerabilities and Exposures) est une occurrence datée dans un produit et une version précise : CVE-2024-50345 dans telle version de tel composant. Chaque CVE est rattaché à un CWE.

La formule à retenir

Le CWE est la maladie, la CVE est le patient. Le CWE-89 (injection SQL) existe depuis toujours et existera encore dans dix ans. Une CVE naît, est corrigée, et se referme.

Autour de la CVE gravitent trois référentiels de priorisation, que nous utilisons dans cet ordre :

  • le CVSS (FIRST) note la gravité théorique de 0 à 10. Il dit la gravité, pas l’urgence.
  • l’EPSS (FIRST également) donne la probabilité qu’une faille soit exploitée dans les trente jours.
  • le catalogue KEV de la CISA liste celles qui sont exploitées pour de vrai, aujourd’hui.

C’est exactement ce que produisent composer audit, npm audit et osv-scanner : une liste de CVE sur les projets, qu’il faut ensuite qualifier et traiter.

L’ASVS : qu’est-ce qui doit être vrai, et à quel niveau

L’Application Security Verification Standard est l’autre référentiel de l’OWASP, et c’est le grand oublié. La version 5.0.0 a été publiée le 30 mai 2025, avec une réorganisation complète de ses chapitres par rapport à la 4.0.

Il aligne environ 350 exigences réparties en 17 chapitres (encodage, validation, front web, API, fichiers, authentification, sessions, autorisation, jetons, OAuth, cryptographie, communication, configuration, protection des données, architecture, journalisation, WebRTC) et sur trois niveaux cumulatifs, que le standard définit lui-même ainsi :

  • L1 : « the minimum requirements to consider when securing an application and represents a critical starting point », soit environ 20 % des exigences. La première ligne de défense.
  • L2 : « most applications should be striving to achieve this level of security », environ 50 % de plus, ce qui porte le total à 70 % des exigences une fois L1 inclus.
  • L3 : les 30 % restants, des mécanismes de défense en profondeur que l’OWASP qualifie lui-même de « hard-to-implement ».

Chaque ligne commence par Verify that. Ce n’est pas une figure de style : c’est ce qui rend l’exigence opposable. La toute première exigence d’autorisation, au niveau minimum, est d’ailleurs documentaire :

8.1.1 (L1) : Verify that authorization documentation defines rules for restricting function-level and data-specific access based on consumer permissions and resource attributes.

Autrement dit : il doit exister un document qui dit qui a le droit de faire quoi. Le Top 10 n’exige jamais de livrable. L’ASVS commence par là.

2. Un même bug, vu par les quatre niveaux

Prenons le cas le plus banal d’une application métier. Un commercial change 4213 en 4214 dans l’URL /devis/4213 et lit le devis d’un autre client.

  • Vu par le Top 10 : c’est A01:2025 Broken Access Control, la famille la plus fréquente en production. Utile pour en parler au client en une phrase et pour justifier la priorité. Insuffisant pour agir.

  • Vu par le CWE : c’est CWE-639, Authorization Bypass Through User-Controlled Key. Le contrôle existe peut-être, mais il porte sur la validité de l’identifiant au lieu de porter sur la propriété de l’objet. Le diagnostic désigne le correctif.

  • Vu par l’ASVS : quatre exigences du chapitre V8 Authorization sont concernées, et chacune est rédigée pour être cochée.

    • 8.2.2 (L1) : contrôle sur la donnée elle-même, pas sur la validité de l’identifiant, pour écarter les référence directe (Id de base de données) et les autorisations de niveau objet rompues (Broken Object Level Authorization - BOLA). C’est exactement le bug de notre exemple : lire /devis/4213 prouve que l’identifiant existe, pas qu’on a le droit de le consulter.
    • 8.3.1 (L1) : la règle d’autorisation s’exécute côté serveur, jamais dans un contrôle que le client pourrait manipuler. Si la vérification vivait dans le JavaScript du formulaire de devis, changer l’URL suffirait à la contourner.
    • 8.2.3 (L2) : accès au niveau du champ. Le commercial a le droit de voir le devis 4213, mais pas d’y lire certains champs spécifique comme la marge ni le coût d’achat.
    • 8.4.1 (L2) : cloisonnement multi-tenant. Une opération d’un client ne peut jamais toucher les données d’un autre.

Les deux dernières sont le vrai apport de l’ASVS. Ni le Top 10 ni le CWE ne vous auraient fait penser au champ « marge », et le cloisonnement multi-client, qui est souvent le premier risque d’une application métier hébergée, n’a aucune existence dans le Top 10.

  • Vu par la CVE : rien. Aucune CVE ne sera jamais publiée pour ce bug, parce qu’il est dans votre code, pas dans un produit distribué. C’est une limite importante : tout ce que votre scanner de dépendances trouve est, par construction, ce que vous n’avez pas écrit.

3. Pourquoi la confusion coûte cher

« Nous couvrons l’OWASP Top 10 » n’engage à rien

C’est une phrase invérifiable. Le Top 10 ne comporte aucun critère de conformité, et la fondation qui le publie le dit elle-même. Personne ne peut auditer cette affirmation, donc personne ne peut vous reprocher de ne pas la tenir, donc elle ne vaut rien.

« Nous tenons ASVS 5.0 niveau L2 sur les chapitres V6, V7 et V8, L1 sur le reste » est d’une autre nature : un RSSI (Responsable de la Sécurité des Systèmes d’Information) peut prendre la liste et la parcourir ligne à ligne. C’est vérifiable, donc c’est engageant et crédible. Et le niveau compte autant que le périmètre : annoncer L1 seul reviendrait à revendiquer le minimum du standard, ce qui n’engagerait guère plus que la phrase précédente.

Le CWE est la seule clé stable

Une édition du Top 10 peut renuméroter ses sections, d’une édition à l’autre, ce qui peut changer la nature et le sens d’un numéro de section. Exemple avec A03:2021 qui désignait les injections, si on le compare avec A03:2025 qui désigne maintenant les défaillances de la chaîne d’approvisionnement logicielle. Un rapport d’audit de 2023 qui cite « A03 » sans préciser l’année d’édition peut devenir ambigu du jour au lendemain.

L’ASVS a fait de même en passant de 4.0 à 5.0. Un exemple frappant sur le chapitre que cet article cite le plus : V8 désignait Data Protection dans la version 4.0.3, et désigne maintenant Authorization en 5.0.0, deux sujets sans rapport entre eux. Ce n’est pas un cas isolé : l’OWASP le documente elle-même, sur les 286 exigences de la 4.0.3, seules 11 restent identiques mot pour mot dans la 5.0.0.

Un numéro de rubrique, que ce soit au Top 10 ou à l’ASVS, n’est donc jamais stable dans le temps : mieux vaut toujours citer l’identifiant CWE correspondant. Le CWE-89, lui, désigne l’injection SQL depuis toujours. C’est pour cette raison que les outils l’utilisent, et c’est pour cette raison que c’est sur lui qu’il faut indexer une base de connaissance interne, jamais sur un identifiant OWASP.

Le piège de la citation sans édition

Citer « A03 » ou « ASVS V4 » dans un cahier des charges, un rapport ou un contrat sans préciser l’édition rend le document ambigu à la révision suivante. Toujours écrire A03:2025 et ASVS 5.0.0.


Partie 2 : la pratique

4. Le point de départ n’est pas un outil, c’est la conception

La tentation, quand on découvre cette pile, est d’aller directement brancher des scanners. C’est une erreur d’ordre. Un outil trouve un défaut dans ce qui a été écrit. Il ne trouve pas une absence de conception. C’est exactement ce que dit la catégorie A06:2025 Insecure Design : une fonctionnalité bancale dès l’origine ne se corrige par aucun correctif rapide.

Le vrai point de départ est de comprendre ces quatre référentiels pour s’en servir pendant le cadrage, quand le coût d’une décision est encore nul.

Concrètement, cela se traduit par un réflexe simple : traiter l’ASVS comme une bibliothèque consultable en cas de doute, pas comme un audit à passer. Une fonctionnalité soulève une question ? On ouvre le chapitre concerné avant d’improviser une réponse.

  • On conçoit un export de données ? Chapitre V1, exigence 1.2.10 sur l’injection de formules dans les fichiers CSV et tableurs. Personne n’y pense spontanément.
  • On ajoute un champ « URL du logo » que le serveur ira télécharger ? Exigence 1.3.6 (L2) : valider contre une liste blanche de protocoles, domaines, chemins et ports. C’est du SSRF (Server-Side Request Forgery), et il est bien plus simple de le cadrer au moment où le champ est ajouter pendant la conception qu’après la mise en production.
  • On ouvre une interface d’administration ? Chapitre V8, exigence 8.4.2 sur les couches de sécurité multiples.
  • On définit qui voit quoi ? Exigence 8.1.1, et son livrable documentaire.

Le Top 10 sert au même moment, mais à un autre usage : il donne la grille de relecture d’un cadrage. Dix questions à se poser sur chaque nouvelle fonctionnalité, dans un ordre qui reflète la fréquence réelle des incidents. C’est un ordre du jour de réunion, pas une checklist de livraison.

Le bon geste

Ne pas apprendre l’ASVS par coeur, personne ne le fait. Savoir qu’il existe, savoir dans quel chapitre chercher, et l’ouvrir au moment de la conception plutôt qu’au moment de l’audit.

5. Le fil complet, sur un cas réel

Prenons un sprint ordinaire sur une application de gestion de devis, avec deux tickets :

  1. ajouter une recherche libre sur la liste des devis ;
  2. permettre le partage d’un devis à un collègue via un lien.

Les deux vont produire un défaut. Notre chaîne d’outils en attrape un entièrement, et ne voit strictement rien du second. C’est ce contraste qui est intéressant.

Cas 1 : le filtre de recherche, attrapé par la chaîne

Le développeur écrit ceci, sans mauvaise intention, parce que la requête paramétrée aurait demandé deux lignes de plus :

// src/Repository/DevisRepository.php
public function searchByReference(string $q): array
{
    return $this->createQueryBuilder('d')
        ->andWhere("d.reference LIKE '%" . $q . "%'")  // concaténation directe
        ->getQuery()
        ->getResult()
    ;
}

Étape 1, le style. make cs lance php-cs-fixer avec le jeu de règles @Symfony. Il reformate, il ne dit rien sur le fond. Ce n’est pas de la sécurité, mais c’est le socle : cela nous permet d’identifier les variantes de style d’écriture au plus tôt et avoir un format d’écriture commun entre tous nos projet.

Étape 2, les types. make phpstan lance PHPStan au niveau 10, le plus élevé, avec les extensions Symfony, Doctrine et PHPUnit. Il valide que $q est bien une string et que le QueryBuilder est correctement utilisé. Tout est vert. PHPStan ne voit rien, et c’est normal : le code est parfaitement bien typé. Il est simplement dangereux.

Étape 3, le flux de données. make psalm lance l’analyse de taint. C’est ici que ça se joue, et le mécanisme mérite d’être compris parce qu’il ne fonctionne pas par détection de motif :

  • le plugin psalm/plugin-symfony déclare les sources : tout ce qui sort de Request, InputBag, ParameterBag, HeaderBag est marqué comme non fiable ;
  • notre fichier maison psalm-taint-stubs.php déclare les sinks Doctrine : createQuery(), et les méthodes where, andWhere, orWhere, having, groupBy du QueryBuilder, plus Connection::executeQuery() et executeStatement() côté DBAL ;
  • Psalm suit alors le flux réel entre les deux, y compris à travers plusieurs appels de méthodes et plusieurs fichiers.

La sortie prend cette forme :

ERROR: TaintedSql - src/Repository/DevisRepository.php:58:24
  Detected tainted SQL
    $q  <- DevisController::index()  <- $request->query->get('q')

C’est la différence de fond avec un scanner à motifs : Psalm ne signale pas « une variable près d’une requête », il signale un chemin prouvé entre une entrée HTTP et une requête SQL. C’est ce qui rend le taux de faux positifs tenable, et ce qui a motivé le remplacement de l’ancien pheromone/phpcs-security-audit, abandonné depuis 2019.

Pourquoi un fichier de stubs maison

Aucun plugin Psalm ne marque le chemin ORM de Doctrine comme un sink SQL. Le plugin communautaire existant ne couvre que le DBAL bas niveau. Sans notre psalm-taint-stubs.php, aucune injection passant par createQuery() ou par le QueryBuilder ne serait détectée : l’analyse tournerait, elle serait valide, et elle ne détecterais rien.

Étape 4, la traduction. Le rapport dit TaintedSql. Voici la chaîne complète :

NiveauCe que ça donne
OutilTaintedSql, ligne 58 de DevisRepository.php
CWECWE-89, Improper Neutralization of Special Elements used in an SQL Command
OWASP Top 10A05:2025 Injection
ASVS1.2.4 (L1) : « Verify that data selection or database queries use parameterized queries, ORMs, entity frameworks, or are otherwise protected from SQL Injection »
VerdictExigence de niveau L1 en défaut : sous le socle minimum, non négociable

Étape 5, la correction.

return $this->createQueryBuilder('d')
    ->andWhere('d.reference LIKE :q')
    ->setParameter('q', '%' . $q . '%')
    ->getQuery()
    ->getResult();

Psalm redevient vert parce que le flux ne traverse plus le sink : la valeur passe par setParameter(), que Doctrine échappe. Et le contrôle reste en place pour la prochaine fois, sans que personne n’ait à s’en souvenir.

Cas 2 : le lien de partage, que personne ne voit

Le second ticket produit ce contrôleur :

#[Route('/devis/partage/{id}', name: 'devis_partage')]
public function partage(Devis $devis): Response
{
    return $this->render('devis/show.html.twig', ['devis' => $devis]);
}

Passons-le dans toute la chaîne :

  • php-cs-fixer : conforme.
  • PHPStan niveau 10 : le typage est irréprochable, le ParamConverter fait son travail. Rien à signaler.
  • Psalm taint : l’identifiant vient bien d’une source non fiable, mais il finit dans un paramètre de requête correctement échappé par Doctrine. Aucun flux dangereux. Rien à signaler.
  • ZAP en scan actif : le crawler visite l’URL avec une session valide, reçoit un 200 OK et une page bien formée. Pour lui, tout va bien.
  • Nuclei : ses templates cherchent des vulnérabilités connues et des expositions typiques, pas votre logique métier. Rien.
  • composer audit, npm audit, osv-scanner : aucune dépendance en cause.

Plusieurs outils, zéro alerte. Et pourtant n’importe quel utilisateur authentifié peut lire tous les devis de tous les clients en incrémentant l’identifiant (c’est l’exemple du devis qu’on a vu plus haut dans la section « 2. Un même bug, vu par les quatre niveaux »).

Il s’agit donc de la CWE-639, mentionné dans le A01:2025 du Top 10, c’est la faille la plus fréquente en production, et notre chaîne d’analyse est complètement aveugle dessus.

Il n’existe qu’un seul contrôle possible, et il faut l’écrire à la main :

public function testPartageRefuseUnAutreClient(): void
{
    $client = static::createClient();
    $client->loginUser($this->getUser('commercial@client-a.fr'));

    $client->request('GET', '/devis/partage/4214'); // devis du client B

    self::assertResponseStatusCodeSame(403);
}

Ce test PHPUnit est la vérification de l’exigence ASVS 8.2.2. Il n’existe pas d’autre moyen : aucun outil ne peut deviner quel utilisateur a le droit de voir quel devis, parce que cette information n’est écrite nulle part ailleurs que dans votre métier.

Ce que les deux cas enseignent

Cas 1 : injection SQLCas 2 : contrôle d’accès
CWECWE-89CWE-639
OWASP Top 10A05:2025 InjectionA01:2025 Broken Access Control
ASVS1.2.4 (L1)8.2.2 (L1)
Détecté parPsalm taint, automatiquementPersonne
Coût du contrôleUne ligne de configuration, une foisUn test à écrire pour chaque règle métier
Régression possible ?Non, le contrôle est permanentOui, si le test n’est pas écrit

La conclusion est inconfortable mais elle est structurante : le risque numéro un du classement est aussi celui que l’outillage ne couvre pas. Toute la valeur se déplace vers la conception, la revue et les tests fonctionnels.

6. Notre chaîne d’outils, et le niveau de référentiel que chacun produit

Voici l’inventaire complet de ce que nous avons mis en place, et surtout ce que chaque brique traite réellement. L’essentiel est packagé dans notre standard-bundle, installé sur tous nos projets Symfony et lancé d’une seule commande, make qa.

Le socle : lisibilité et types

OutilCe qu’il regardeCe qu’il produit
php-cs-fixer (règles @Symfony)Le formatage, sur bin/, config/, public/, src/, tests/Aucun identifiant de sécurité. Le socle qui garanti l’uniformité du style d’écriture de nos code PHP
Lints natifs PHP, Twig, YAML, conteneurLa syntaxe et les définitions de servicesDes erreurs de build, avant tout le reste
PHPStan niveau 10Les types, sans exécuter le code, via les extensions Symfony, Doctrine et PHPUnitDes erreurs de typage. Indirectement, beaucoup de bugs de logique

Le niveau 10 est le niveau maximum de PHPStan aujourd’hui, et c’est notre réglage par défaut sur tout nouveau projet. Sur une reprise de code existant, nous conservons ce niveau et gelons l’existant dans une baseline plutôt que de descendre le curseur : tout nouveau développement démarre ainsi directement aux règles les plus strictes puis nous rattrappons le retard progressivement en maintenance technique.

La sécurité par présence : les appels interdits

PHPStan porte aussi la partie que l’analyse de flux ne peut structurellement pas traiter : une fonction dangereuse l’est indépendamment de ce qui la traverse. L’extension phpstan-disallowed-calls bloque à la compilation :

Appel interditMotifSe rattache à
eval, assertÉvaluation de chaîne comme du code PHPCWE-95, A05:2025
exec, system, passthru, shell_exec, popenExécution de commande système, à remplacer par le composant Process de SymfonyCWE-78, A05:2025
md5, sha1Algorithmes cryptographiquement cassésCWE-327 et CWE-328, A04:2025
phpinfoDivulgation de la configuration du serveurCWE-200, A01:2025
error_reporting, ini_setConfiguration PHP au runtime au lieu de l’environnementA02:2025

Chaque interdiction porte un message qui explique quoi faire à la place. Un développeur qui bute dessus obtient la consigne au moment où elle lui sert, pas dans un wiki qu’il n’ouvrira pas.

La sécurité par flux : l’analyse de taint

BriqueRôle
vimeo/psalmLe moteur, configuré en runTaintAnalysis="true" et errorLevel="8"
psalm/plugin-symfonyLes sources : entrées HTTP Symfony. Et des sinks XSS : Twig et Response
psalm-taint-stubs.php (maison)Les sinks SQL de Doctrine ORM et DBAL
psalm-taint-baseline.xmlLe gel de l’existant sur une reprise de projet

Le réglage qui surprend

errorLevel="8" est le niveau le plus permissif de Psalm : chez lui, 1 est le plus strict, l’inverse de PHPStan. C’est volontaire. Nous coupons toute la vérification de types côté Psalm pour qu’il n’entre pas en concurrence avec PHPStan, qui reste notre seul type-checker. Les alertes de taint, elles, remontent quel que soit le niveau.

Ce que l’analyse de taint couvre au-delà de l’injection SQL et du XSS : injection de commande, injection LDAP, injection d’en-tête HTTP, désérialisation d’objet, inclusion de fichier, injection de cookie, et SSRF via curl_init, curl_setopt et getimagesize. Soit une bonne part de A05:2025, et un morceau de A01:2025.

Les dépendances et l’infrastructure

OutilPérimètreCe qu’il produit
composer audit --abandoned=reportDépendances PHP, plus les paquets abandonnésDes CVE et des avis de sécurité. A03:2025
npm auditDépendances JavaScript du frontDes CVE. A03:2025
osv-scannerMulti-écosystème, y compris les couches d’une image Docker construiteDes CVE sur ce que Composer et npm ne voient pas. A03:2025
hadolintLe Dockerfile, avant constructionDes règles de bonne pratique. A02:2025
dockleL’image construite, pas sa recetteConfiguration de l’image, secrets embarqués. A02:2025

Ce que ces trois outils ont de précieux tient en une phrase : ils ne parlent que de vos versions. composer audit lit votre composer.lock et confronte chaque paquet installé, dans la version installée, à la base d’avis de sécurité. Il ne vous signalera jamais une faille d’un produit que vous n’utilisez pas, ni d’une version que vous n’avez plus. C’est toute la différence avec le bulletin de sécurité générique qui circule par mail et qu’il faut confronter à la main à son parc : ici, le tri est fait, et il est fait par la machine.

Le volume d’alertes n’est pas le même côté PHP et côté JavaScript, mais pas pour la raison qu’on croit. Les bases d’avis sont comparables : au 8 septembre 2026, la GitHub Advisory Database compte 7 318 avis vérifiés pour npm et 6 455 pour Composer, soit à peine 13 % d’écart. Il ne se publie donc pas moins de failles côté PHP.

L’écart se joue sur la surface exposée. Packagist recense 466 898 paquets là où le registre npm en compte plusieurs millions, et surtout un arbre de dépendances npm est incomparablement plus profond : installer un paquet npm revient à faire confiance à 79 autres paquets en moyenne par transitivité, selon l’étude de référence présentée à USENIX Security. Le site que vous lisez est statique et sans back-end : il déclare 44 dépendances directes, et en installe 1 613. Le back-end Symfony d’une application métier tourne, lui, autour de quelques centaines. À base d’avis équivalente, ce n’est pas le même nombre de signalements à traiter.

Ces alertes ne sont pas moins graves pour autant, elles portent sur autre chose. L’essentiel de ce que remonte npm audit concerne les utilitaires de build (webpack, vite, …), qui ne part jamais en production. On en conclut trop vite que l’enjeu est mineur. Ce code s’exécute sur le poste des développeurs et dans la CI, là où vivent les jetons de déploiement et les clés d’API, et c’est exactement la cible des attaques de chaîne d’approvisionnement de ces dernières années. Le risque n’est pas plus petit, il vise un autre actif.

En résumé, pour extraire l'essentiel

Notre réponse par défaut, c’est de mettre à jour. Aller vérifier précisément si notre code emprunte réellement le chemin vulnérable, coûte du temps d’expert : presque toujours plus que la montée de version elle-même. Nous montons donc, sans enquête préalable, et nous ne qualifions que ce qui résiste, à savoir une montée bloquée par un changement incompatible, une dépendance abandonnée ou un correctif qui n’existe pas.

Nous avons fait ce choix car nous avons construit un système pour le permettre Mettre à jour sereinement est possible car nous nous concentrons sur des technologies que nous maitrisons dans la durée, réalisons des tests automatisés qui permettent de détecter une régression en quelques minutes et investissons dans nos outils notamment nos pipelines de CI. Ceux qui n’ont pas cette base ont plus d’hésitation que nous pour appliquer les mises à jour.

L’application en fonctionnement

OutilAngleCe qu’il produit
ZAP ProxyDAST : interroge l’application déployée depuis l’extérieurDes alertes portant un cweid. Terrain de prédilection : A02:2025
NucleiTemplates ciblés sur des vulnérabilités et expositions connuesDes correspondances classées par cwe-id et cve-id. A02 et A03

Ces deux outils sont ceux qui rendent le mieux visible le rôle pivot du CWE : ils émettent un identifiant CWE, pas une catégorie OWASP. Sans la table de correspondance, impossible de dire à un client « ce scan a couvert telle partie du Top 10 ».

Le comportement métier

OutilCe qu’il vérifieCatégories couvertes
PHPUnit, avec dama/doctrine-test-bundle et liip/test-fixtures-bundleLes parcours applicatifs, y compris rejoués avec un utilisateur non autoriséA01:2025, A06:2025, A10:2025
VitestLa logique du front Vue, et notamment les règles qui ne doivent pas y vivre seulesAppui sur A01:2025

C’est le seul poste de la chaîne qui couvre A01, et c’est le seul dont le coût est proportionnel au nombre de règles métier. Il ne se délègue à aucun outil.

Les secrets et l’humain

LastPass porte les identifiants d’infrastructure et les accès partagés, avec les accès nominatifs et la révocation au départ d’une personne. Ce n’est pas un outil de code, et c’est précisément l’intérêt : le Top 10 ne regarde que l’application. Un mot de passe transmis par message, un compte partagé entre trois personnes ou un accès jamais révoqué ne produiront jamais la moindre alerte, et restent la porte d’entrée de la majorité des attaques.

L’orchestration

Tout ceci est enchaîné par une seule commande, make qa, qui lance dans l’ordre : PHPStan, le checkstyle, Psalm avec sa baseline, les quatre lints, et la validation Composer avec son audit de CVE. La même commande tourne sur le poste du développeur et dans la CI GitLab, ce qui supprime le classique « chez moi ça passe ». Les scans dynamiques, plus lents, tournent quant à eux sur des pipelines programmées ou en manuel plutôt qu’à chaque commit.

La vue d’ensemble

Risque OWASP Top 10:2025Ce qui le couvre chez nous
A01 Contrôle d’accès défaillantPHPUnit et Vitest. Psalm sur le seul volet SSRF
A02 Mauvaise configurationZAP, Nuclei, hadolint, dockle, l’extension phpstan-disallowed-calls sur ini_set
A03 Chaîne d’approvisionnementcomposer audit, npm audit, osv-scanner
A04 Défaillances cryptographiquesPHPStan sur md5 et sha1, ZAP sur le transport. Le stockage se vérifie en revue
A05 InjectionPsalm taint, PHPStan sur les appels interdits, ZAP en scan actif
A06 Conception non sécuriséeAucun outil. Cadrage et revue
A07 Défaillances d’authentificationZAP partiellement. La politique reste un arbitrage
A08 Défauts d’intégritéConfiguration de la pipeline, fichiers de verrouillage commités, politique de contenu
A09 Journalisation et alerteInvisible de l’extérieur. Se constate côté exploitation
A10 Conditions exceptionnellesZAP partiellement, PHPUnit sur le comportement métier dégradé

Le niveau ASVS que nous tenons, et celui qui se décide avec vous

L1 est notre point de départ sur tous les projets. Ce sont les exigences dont l’absence se paie tout de suite : la requête paramétrée, l’autorisation rejouée côté serveur, le document qui dit qui a le droit de faire quoi. La chaîne décrite plus haut en tient une partie toute seule, le reste se tient au cadrage et en revue. Sur une reprise de code, c’est ce que la baseline sert à résorber en premier, avant tout le reste.

Au-delà, c’est un arbitrage, et il vous appartient. Nous travaillons au temps passé : chaque exigence tenue est du temps qui ne va pas à une fonctionnalité. Notre rôle n’est donc pas d’imposer un standard intégral, c’est de dire ce que chaque cran coûte et ce qu’il évite, pour que vous décidiez en connaissance de cause. Une application de gestion interne utilisée par cinq personnes et un portail ouvert à trois mille clients finaux n’appellent manifestement pas le même investissement. Le purisme qui monterait les deux au même niveau ne protège personne : il consomme simplement du budget qui aurait pu être utile ailleurs.

L2 se choisit donc chapitre par chapitre, jamais un un seul bloc. Les questions que nous posons au cadrage pour savoir lesquels méritent l’effort :

Ce que fait l’applicationCe qui mérite alors de monter en L2
Plusieurs clients cohabitent dans la même baseV8 Autorisation, et le cloisonnement 8.4.1
Elle porte des données personnelles au-delà de l’identité : santé, RH, situation financièreV14 Protection des données, et l’accès au champ 8.2.3
Elle ouvre des comptes hors de l’organisation : clients finaux, sous-traitantsV6 Authentification, V7 Sessions
Elle est devenue le système de référence du métier, et son arrêt arrête l’activitéV16 Journalisation et traçabilité

Ce n’est pas une grille automatique. C’est une conversation, tenue avant d’écrire la première ligne, et dont la sortie est une phrase vérifiable du type « L2 sur V8 et V14, L1 sur le reste ». Le niveau ne se déclare jamais seul : il se déclare avec son périmètre, faute de quoi il ne veut rien dire.

L3, nous ne le revendiquons pas. Les 30 % restants sont des contrôles que l’OWASP qualifie lui-même de difficiles à implémenter, et la plupart supposent un contexte que nos projets n’ont pas : cloisonnement réseau, revue formelle, contraintes réglementaires sectorielles. L’annoncer sans audit externe ne voudrait rien dire. Un client qui en a réellement besoin le cadre avec un auditeur tiers, et nous construisons avec lui.

7. Ce que cette chaîne ne couvre pas

Une page qui listerait des outils sans dire où ils s’arrêtent serait exactement le genre de contenu que cet article critique en introduction. Voici les angles morts, assumés.

A01 et A06 restent manuels. Le risque le plus fréquent et le risque le plus coûteux à corriger sont tous deux hors de portée de l’analyse automatique. Aucune dépense d’outillage ne les traite.

Une baseline n’est pas une absolution. phpstan-baseline.neon et psalm-taint-baseline.xml gèlent l’existant pour qu’une reprise de projet ne bloque pas la CI dès le premier jour. C’est un instrument de dette technique, pas un correctif. Une baseline qui ne décroît pas d’un trimestre à l’autre est un signal, et il se lit.

Le piège de la baseline

Une CI verte sur un projet dont la baseline contient trois cents entrées de taint gelées ne dit pas « ce code est sûr ». Elle dit « ce code n’est pas devenu plus dangereux depuis la dernière fois ». Les deux affirmations n’ont rien à voir, dommage que tant de personne ne comprenne que la première.

Aucun scanner ne sait si votre code atteint la faille. composer audit et npm audit ne remontent que des CVE qui touchent vos paquets, dans vos versions : ce tri-là est déjà fait. Ce qu’ils ne savent pas dire, c’est si la fonction vulnérable est réellement appelée par votre application. La qualification reste donc un travail humain, et c’est elle qui évite à la fois la panique et l’accoutumance aux alertes.

La journalisation ne se vérifie pas depuis l’extérieur. A09 se constate côté exploitation, et surtout dans le régime convenu : un outil qui alerte sans destinataire qui regarde ne couvre rien du tout.

Conclusion

L’OWASP Top 10 est une excellente porte d’entrée de sensibilisation. Pour une équipe qui n’a jamais mis le sujet à l’ordre du jour, il fait un travail que rien ne remplace : il nomme dix risques, il les hiérarchise, il donne un vocabulaire commun à des gens qui n’en avaient aucun. Comme outil pédagogique, il est difficile à battre, et c’est déjà beaucoup pour qui part de zéro.

Il s’arrête là, et il ne s’en cache pas : aucun critère de conformité, aucune exigence vérifiable, aucun moyen de prouver quoi que ce soit. Un logiciel robuste ne se construit pas avec une liste de sensibilisation. Le présenter comme un référentiel de vérification c’est s’arrêter très en dessous de ce que le sujet demande.

Ce qui rend l’ensemble opérationnel, c’est de comprendre la logique cachée derrière : une alerte d’outil porte un CWE, le CWE se range dans une catégorie du Top 10 pour la conversation, et se traduit en exigence ASVS pour l’engagement. Chaque maillon répond à une question que les autres ne posent pas.

Et le résultat le plus utile de cette mise à plat est peut-être celui-ci : une fois la chaîne montée, on voit précisément où elle s’arrête. Une chaine d’outils qui ne détectent pas un contrôle d’accès manquant, ce n’est pas un échec de l’outillage, c’est une information de pilotage. Elle dit où mettre le temps humain, et c’est exactement ce qu’on attend d’une démarche de sécurité : arrêter de deviner.

Ce que tout cela dit du coût réel d’un logiciel

Si cet article ne laisse qu’une impression, nous aimerions que ce soit celle-ci : la sécurité d’une application n’est pas un état atteint à la livraison, c’est un travail d’exploitation qui ne s’arrête jamais. Les CVE tombent après la mise en production. Les dépendances vieillissent après. Les référentiels se renumérotent après. Rien de ce que cet article décrit ne se règle une fois pour toutes, et c’est précisément ce qu’un forfait de maintenance technique finance : le temps de regarder, de monter les versions, de garder la chaîne verte et d’instruire ce qui résiste.

C’est aussi pourquoi nous ne comprenons pas qu’un prestataire puisse développer un logiciel sans assurer ensuite sa maintenance. Ce n’est pas d’abord une question commerciale, c’est une question de compétence. Les pratiques décrites ici ne s’apprennent pas dans un standard, elles s’apprennent en exploitant : en encaissant une régression après une montée de version, en découvrant qu’un test manquait, en réparant un pipeline dans lequel personne n’osait plus déployer. Qui ne maintient jamais ne rencontre jamais ces situations, n’en tire donc aucune leçon, et livre du code qui n’a jamais eu à tenir dans la durée. Le développement seul est un métier plus facile, et cela se voit.

Rien de ce raisonnement n’est propre au développement sur mesure, et c’est encore plus vrai de sa partie la plus difficile : la conception. Reprenez le tableau de couverture plus haut. Nos outils attrapent l’injection et la mauvaise configuration, mais ils ne voient rien de A01 ni de A06, parce que ces deux risques ne sont pas des défauts d’écriture : ce sont des décisions prises avant d’écrire. Qui a le droit de lire quel devis, quel champ reste invisible pour un commercial, ce qu’un client ne doit jamais pouvoir atteindre chez un autre : aucun outil ne le devine, et aucune plateforme ne le décide à votre place.

C’est là que le no-code et le vibe coding se jugent, pas sur la vitesse de production. Les deux suppriment réellement une classe de risques : la plateforme paramètre ses requêtes, l’assistant écrit du code qui passe par un ORM, et l’injection SQL quitte largement le paysage. Les deux laissent en revanche intégralement ce que nos outils ne savent pas voir. Un modèle de langage produira sans broncher une route sans contrôle de permission si rien dans la demande ne l’a exigé, et il ne posera pas la question à votre place. Un constructeur no-code pourra livrer une vue accessible à tous les membres de l’espace parce que c’est le réglage par défaut, et personne ne s’en apercevra tant qu’un utilisateur n’aura pas essayé.

Un nocodeur, ou un développeur assisté, qui n’intègre pas ces questions dans sa conception ne fait donc pas le même travail en plus rapide : il fait un travail incomplet, dont la partie manquante est précisément celle qui coûte le plus cher à rattraper. La bonne question n’a jamais été « avons-nous du code ? », mais « qui a décidé qui a le droit de faire quoi, et qui s’occupe de ce logiciel la deuxième, troisième, quatrième… année ? »


Liens utiles

Nicolas Bastien
Nicolas Bastien Expert développement web
Mots clés :
#Technique#Qualité#Conception

Articles similaires

Vous avez un projet ?

Contactez-nous pour savoir comment nous pouvons vous aider.