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.
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 :
| CWE | Ce que ça décrit | Ce qu’il faut faire |
|---|---|---|
| CWE-862 | Missing Authorization : aucun contrôle n’existe sur la route | Ajouter un contrôle |
| CWE-863 | Incorrect Authorization : le contrôle existe mais se trompe | Corriger la règle |
| CWE-639 | Le contrôle porte sur une clé fournie par l’utilisateur | Vé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.
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/4213prouve 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.
- 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
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.
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.
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 :
- ajouter une recherche libre sur la liste des devis ;
- 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-symfonydéclare les sources : tout ce qui sort deRequest,InputBag,ParameterBag,HeaderBagest marqué comme non fiable ; - notre fichier maison
psalm-taint-stubs.phpdéclare les sinks Doctrine :createQuery(), et les méthodeswhere,andWhere,orWhere,having,groupByduQueryBuilder, plusConnection::executeQuery()etexecuteStatement()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.
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 :
| Niveau | Ce que ça donne |
|---|---|
| Outil | TaintedSql, ligne 58 de DevisRepository.php |
| CWE | CWE-89, Improper Neutralization of Special Elements used in an SQL Command |
| OWASP Top 10 | A05:2025 Injection |
| ASVS | 1.2.4 (L1) : « Verify that data selection or database queries use parameterized queries, ORMs, entity frameworks, or are otherwise protected from SQL Injection » |
| Verdict | Exigence 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 OKet 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 SQL | Cas 2 : contrôle d’accès | |
|---|---|---|
| CWE | CWE-89 | CWE-639 |
| OWASP Top 10 | A05:2025 Injection | A01:2025 Broken Access Control |
| ASVS | 1.2.4 (L1) | 8.2.2 (L1) |
| Détecté par | Psalm taint, automatiquement | Personne |
| Coût du contrôle | Une ligne de configuration, une fois | Un test à écrire pour chaque règle métier |
| Régression possible ? | Non, le contrôle est permanent | Oui, 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
| Outil | Ce qu’il regarde | Ce 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, conteneur | La syntaxe et les définitions de services | Des erreurs de build, avant tout le reste |
| PHPStan niveau 10 | Les types, sans exécuter le code, via les extensions Symfony, Doctrine et PHPUnit | Des 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 interdit | Motif | Se rattache à |
|---|---|---|
eval, assert | Évaluation de chaîne comme du code PHP | CWE-95, A05:2025 |
exec, system, passthru, shell_exec, popen | Exécution de commande système, à remplacer par le composant Process de Symfony | CWE-78, A05:2025 |
md5, sha1 | Algorithmes cryptographiquement cassés | CWE-327 et CWE-328, A04:2025 |
phpinfo | Divulgation de la configuration du serveur | CWE-200, A01:2025 |
error_reporting, ini_set | Configuration PHP au runtime au lieu de l’environnement | A02: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
| Brique | Rôle |
|---|---|
vimeo/psalm | Le moteur, configuré en runTaintAnalysis="true" et errorLevel="8" |
psalm/plugin-symfony | Les 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.xml | Le gel de l’existant sur une reprise de projet |
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
| Outil | Périmètre | Ce qu’il produit |
|---|---|---|
composer audit --abandoned=report | Dépendances PHP, plus les paquets abandonnés | Des CVE et des avis de sécurité. A03:2025 |
npm audit | Dépendances JavaScript du front | Des CVE. A03:2025 |
osv-scanner | Multi-écosystème, y compris les couches d’une image Docker construite | Des CVE sur ce que Composer et npm ne voient pas. A03:2025 |
hadolint | Le Dockerfile, avant construction | Des règles de bonne pratique. A02:2025 |
dockle | L’image construite, pas sa recette | Configuration 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.
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
| Outil | Angle | Ce qu’il produit |
|---|---|---|
| ZAP Proxy | DAST : interroge l’application déployée depuis l’extérieur | Des alertes portant un cweid. Terrain de prédilection : A02:2025 |
| Nuclei | Templates ciblés sur des vulnérabilités et expositions connues | Des 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
| Outil | Ce qu’il vérifie | Catégories couvertes |
|---|---|---|
PHPUnit, avec dama/doctrine-test-bundle et liip/test-fixtures-bundle | Les parcours applicatifs, y compris rejoués avec un utilisateur non autorisé | A01:2025, A06:2025, A10:2025 |
| Vitest | La logique du front Vue, et notamment les règles qui ne doivent pas y vivre seules | Appui 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:2025 | Ce qui le couvre chez nous |
|---|---|
| A01 Contrôle d’accès défaillant | PHPUnit et Vitest. Psalm sur le seul volet SSRF |
| A02 Mauvaise configuration | ZAP, Nuclei, hadolint, dockle, l’extension phpstan-disallowed-calls sur ini_set |
| A03 Chaîne d’approvisionnement | composer audit, npm audit, osv-scanner |
| A04 Défaillances cryptographiques | PHPStan sur md5 et sha1, ZAP sur le transport. Le stockage se vérifie en revue |
| A05 Injection | Psalm taint, PHPStan sur les appels interdits, ZAP en scan actif |
| A06 Conception non sécurisée | Aucun outil. Cadrage et revue |
| A07 Défaillances d’authentification | ZAP 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 alerte | Invisible de l’extérieur. Se constate côté exploitation |
| A10 Conditions exceptionnelles | ZAP 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’application | Ce qui mérite alors de monter en L2 |
|---|---|
| Plusieurs clients cohabitent dans la même base | V8 Autorisation, et le cloisonnement 8.4.1 |
| Elle porte des données personnelles au-delà de l’identité : santé, RH, situation financière | V14 Protection des données, et l’accès au champ 8.2.3 |
| Elle ouvre des comptes hors de l’organisation : clients finaux, sous-traitants | V6 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.
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
- OWASP Top 10 : le projet officiel
- OWASP ASVS : le standard de vérification
- CWE : la liste du MITRE
- Psalm : la documentation de l’analyse de sécurité
- PHPStan : les niveaux de règles
- GitHub Advisory Database : les avis de sécurité, filtrables par écosystème
- Small World with High Risks : l’étude de référence sur les dépendances npm, USENIX Security 2019
- standard-bundle : nos commandes et notre configuration
- Auditer la sécurité en blackbox avec ZAP Proxy
- Nuclei : scanner de vulnérabilités par templates
- Auditer vos Dockerfile : sécurité, CVE et bonnes pratiques