Aller au contenu principal
own2pwn
OWASP Dependency-Check et npm audit : deux scanners SCA au banc d'essai

OWASP Dependency-Check et npm audit : deux scanners SCA au banc d'essai

On a lancé OWASP Dependency-Check sur un projet Java réel et npm audit sur un projet Node réel. Sortie brute et comptage honnête : alertes, vraies vulnérabilités et failles réellement atteignables.

own2pwn11 min de lecture

Un inventaire des dépendances à surveiller ?

Le module SCA d'own2pwn croise vos composants avec les vulnérabilités connues, en continu.

Voir le module SCA

Deux dépôts, deux écosystèmes, deux scanners. D'un côté un projet Node.js avec sept dépendances directes volontairement datées ; de l'autre un dossier de dix archives Java connues pour leurs failles, de Log4j 2.14.1 à commons-text 1.9. Sur le premier, npm audit, livré avec npm. Sur le second, OWASP Dependency-Check, le scanner de composition libre le plus installé du monde Java. Les deux répondent à la même question : quelles vulnérabilités connues traînent dans les briques que je n'ai pas écrites ?

Ce qui suit n'est pas une compilation de documentation. Les commandes ont vraiment tourné, les sorties sont recopiées telles quelles, et le chiffre qui compte n'est jamais celui que l'outil met en gros. Un scanner de software composition analysis annonce un total d'alertes ; le nombre de vraies vulnérabilités est plus bas, et le nombre de failles réellement atteignables plus bas encore. Ce sont trois nombres différents, et c'est tout l'intérêt de les mesurer sur du vrai code.

Software composition analysis, en une phrase

Le software composition analysis, ou SCA, consiste à inventorier les composants tiers d'une application et à confronter cet inventaire aux bases de vulnérabilités connues. Vous n'analysez pas votre code : vous cataloguez les bibliothèques qu'il embarque, vous en déterminez la version, et vous demandez si cette version précise fait l'objet d'un avis de sécurité. C'est le pendant, côté dépendances, de ce qu'un scanner de vulnérabilité fait sur une infrastructure. Le principe est simple ; la difficulté est entièrement dans l'identification exacte du composant, et c'est là que les deux outils divergent.

npm audit s'appuie sur l'arbre de dépendances résolu par npm. Il connaît le nom canonique et la version exacte de chaque paquet, parce que ces informations sont dans le package-lock.json. Il compare chaque entrée à la base d'avis GitHub (GHSA) et signale toute version comprise dans une plage vulnérable déclarée. Zéro devinette : l'identité du composant est certaine. Le revers, c'est qu'il ne sait faire ça que pour npm.

OWASP Dependency-Check n'a pas ce luxe. Face à un fichier log4j-core-2.14.1.jar, il n'a aucune garantie sur ce que contient réellement l'archive. Il extrait donc des indices, ce qu'il appelle des evidence : le nom de fichier, les entrées du manifeste, les métadonnées du pom.xml embarqué, les noms de paquets Java. À partir de ces indices, il fabrique un ou plusieurs identifiants CPE (Common Platform Enumeration), la nomenclature normalisée des produits logiciels, puis interroge la base NVD du NIST. Cette souplesse lui permet de couvrir Java, .NET, Python, Ruby, Node et d'autres ; elle est aussi la source de ses faux positifs.

deux-methodes-sca
npm audit
Arbre résolu par npm
Nom canonique + version exacte lus dans le lockfile, comparés à la base d'avis GitHub.
Résultat
Composant certain, avis certain
Pas de devinette sur l'identité du paquet.
Dependency-Check
Indices extraits de l'archive
Nom de fichier, manifeste, pom embarqué, paquets Java, reconstruits en identifiant CPE.
Résultat
Composant deviné, CVE probables
Large couverture, mais faux positifs quand l'indice est ambigu.
Deux façons d'identifier un composant : la version exacte tirée du lockfile, ou l'indice reconstruit puis mappé sur un CPE.

npm audit sur un projet Node réel

Le projet de test est minimal et honnête : un package.json qui déclare sept dépendances directes figées à des versions anciennes mais plausibles, du genre de ce qu'on trouve dans un dépôt laissé sans mise à jour deux ou trois ans.

json
{
  "name": "audit-demo",
  "dependencies": {
    "express": "4.16.0",
    "lodash": "4.17.4",
    "minimist": "1.2.0",
    "axios": "0.21.0",
    "jsonwebtoken": "8.3.0",
    "handlebars": "4.0.11",
    "moment": "2.19.3"
  }
}

Après npm install, l'arbre complet compte 93 paquets : les sept déclarés, plus tout ce qu'ils tirent derrière eux. Un simple npm audit suffit, sans option. Voici la fin de sa sortie, recopiée avec npm 10.9.3 sous Node 22.20 :

text
# npm audit report

axios  <=0.32.0
Severity: high
Axios vulnerable to Server-Side Request Forgery - GHSA-4w2v-q235-vp99
Axios Cross-Site Request Forgery Vulnerability - GHSA-wf5p-g6vw-rhxx
... (25 avis listés pour le seul axios) ...

handlebars  <=4.7.8
Severity: critical
Remote code execution in handlebars - GHSA-q42p-pg8m-cqh6
...

optimist  >=0.6.0
Severity: critical
Depends on vulnerable versions of minimist
node_modules/optimist

14 vulnerabilities (3 low, 7 high, 4 critical)

Quatorze paquets vulnérables, donc, répartis en trois de gravité basse, sept élevée et quatre critique. Le comptage par paquet cache le volume réel d'avis : axios à lui seul accumule vingt-cinq entrées de la base GHSA, des SSRF aux fuites de Proxy-Authorization en passant par une dizaine de pollutions de prototype. handlebars, lodash, minimist et optimist portent les quatre alertes critiques.

Un détail sépare déjà l'alerte de l'action : sur ces quatorze paquets, sept sont des dépendances directes (celles du package.json) et sept sont transitives, tirées par les précédentes. optimist, par exemple, n'a jamais été demandé : c'est handlebars 4.0.11 qui l'embarque, et optimist embarque à son tour un minimist vulnérable. Corriger optimist n'a pas de sens en soi ; il faut monter handlebars, son parent. C'est la mécanique classique de l'attaque par la chaîne d'approvisionnement : la faille est chez un composant que vous n'avez jamais choisi.

Ce que npm audit ne dit pas

Chacune de ces quatorze alertes est vraie au sens strict : la version installée est bien dans la plage vulnérable de l'avis. Mais npm audit ne vérifie pas que le code vulnérable est appelé. La ReDoS de minimist suppose que vous parsez des arguments attaquant-contrôlés ; sur une application qui n'expose pas cette entrée, la version est présente sans que la faille soit atteignable. L'outil compte des composants, pas des chemins d'attaque.

OWASP Dependency-Check sur un projet Java réel

Côté Java, pas de lockfile à interroger : un dossier lib/ avec dix archives récupérées depuis Maven Central, toutes choisies pour un passé chargé. Log4j 2.14.1 (la version de Log4Shell), commons-text 1.9, commons-collections 3.2.1, jackson-databind 2.9.8, snakeyaml 1.30, spring-core et spring-web 5.2.0, httpclient 4.5.12 et une vieille Guava. L'installation de Dependency-Check 12.1.0 tient en une décompression ; le scan se lance ainsi :

bash
# dependency-check 12.1.0, scan d'un dossier de jars
dependency-check.sh \
  --scan ./lib \
  --out ./rapport \
  --format HTML --format JSON \
  --project "audit-java-demo"

La première exécution télécharge toute la NVD

Sans clé API NVD, le premier scan synchronise l'intégralité de la base du NIST, soit plus de 385 000 enregistrements CVE, et l'outil prévient lui-même que cela peut être très long. Une clé API gratuite du NIST accélère fortement cette étape ; en intégration continue, la base se met en cache et se rafraîchit ensuite par delta. Ce coût de premier démarrage est propre à Dependency-Check : npm audit interroge un service en ligne et n'a rien à télécharger.

Sur cette machine, sans clé, la synchronisation a pris une heure (lancée à 07h10, rapport écrit à 08h10, 237 Mo de base sur le disque à l'arrivée). Elle a même levé une exception en route, une CVE dont l'URL de référence dépassait la largeur de colonne prévue par l'outil, sans interrompre l'import. L'analyse proprement dite, elle, a duré sept secondes. Une fois le rapport JSON déplié archive par archive, voilà ce qu'il contient :

text
Dependency-Check 12.1.0, projet audit-java-demo, 10 dépendances analysées

archive                          CVE   pire gravité    CPE retenu
commons-collections-3.2.1.jar      1   CRITICAL        apache:commons_collections:3.2.1
commons-text-1.9.jar               1   CRITICAL        apache:commons_text:1.9
guava-24.1.1-jre.jar               2   HIGH            google:guava:24.1.1
httpclient-4.5.12.jar              1   MEDIUM          apache:httpclient:4.5.12
jackson-databind-2.9.8.jar        56   CRITICAL (14)   fasterxml:jackson-databind:2.9.8 (+2 CPE)
log4j-api-2.14.1.jar               6   MEDIUM          apache:log4j:2.14.1
log4j-core-2.14.1.jar             10   CRITICAL (2)    apache:log4j:2.14.1
snakeyaml-1.30.jar                 7   CRITICAL        snakeyaml_project:snakeyaml:1.30
spring-core-5.2.0.RELEASE.jar     24   CRITICAL (2)    pivotal_software:spring_framework:5.2.0 (+2 CPE)
spring-web-5.2.0.RELEASE.jar      25   CRITICAL (3)    pivotal_software:spring_framework:5.2.0 (+3 CPE)

Total : 133 entrées de vulnérabilité, 103 CVE distinctes
Known Exploited Vulnerabilities (catalogue CISA) : 4 entrées, 3 CVE distinctes

Sur les têtes d'affiche, l'outil ne se trompe pas. Log4Shell, CVE-2021-44228, est bien posée sur log4j-core 2.14.1 et signalée comme exploitée dans la nature. Text4Shell, CVE-2022-42889, est sur commons-text 1.9. La désérialisation de commons-collections 3.2.1, celle qui a nourri des années de chaînes de gadgets, et le constructeur non restreint de snakeyaml 1.30 sont là aussi. Quatre archives, quatre CVE critiques, quatre bonnes réponses.

Le problème est dans le total. 133 entrées pour 103 CVE distinctes, donc trente doublons : spring-core et spring-web partagent 24 CVE, log4j-api et log4j-core en partagent 6. Dependency-Check raisonne par fichier, et un framework découpé en plusieurs archives voit chacune de ses CVE comptée autant de fois qu'il y a d'archives. Spring4Shell, CVE-2022-22965, apparaît ainsi deux fois, marquée exploitée deux fois. Sur un vrai projet Spring qui embarque quinze archives spring-*, le même avis se répète quinze fois dans le rapport. Un tableau de bord qui additionne les lignes annonce alors un chiffre sans rapport avec la réalité.

Le talon d'Achille de Dependency-Check : le CPE matching

Un identifiant CPE désigne un produit dans la nomenclature du NIST, pas un artefact Maven. Pour la NVD, cpe:2.3:a:apache:log4j:2.14.1 recouvre toute la famille Log4j 2 : le moteur log4j-core, l'API log4j-api, le pont de compatibilité log4j-1.2-api et le reste. Dependency-Check extrait de chaque archive les mêmes indices (éditeur apache, produit log4j, version 2.14.1), fabrique le même CPE, et récupère donc le même paquet de CVE pour les deux fichiers. Six CVE de 2025 et 2026 se retrouvent ainsi collées à l'identique sur log4j-api et log4j-core. J'en ai ouvert trois.

CVE-2026-34479 décrit un défaut d'échappement XML dans Log4j1XmlLayout, une classe du pont log4j-1.2-api : une archive qui n'est pas dans le dossier. Faux positif sur les deux fichiers. CVE-2025-68161 concerne le Socket Appender de log4j-core, qui ne vérifie pas le nom d'hôte TLS : juste sur log4j-core, faux sur log4j-api. CVE-2026-49844 touche la sérialisation JSON de MapMessage dans log4j-api : juste sur log4j-api, faux sur log4j-core. Trois CVE, six lignes de rapport, deux exactes, quatre fausses. Ce n'est pas un bug de l'outil, c'est la granularité de la NVD, qui ne descend pas au niveau de l'artefact. Détail qui intrigue : Log4Shell elle-même n'est posée que sur log4j-core. L'outil sait donc parfois trancher, pas toujours.

cpe-log4j
CVECe qu'elle touche réellementVerdict
CVE-2026-34479Le pont log4j-1.2-api, une archive absente du dossier.Faux positif sur les deux fichiers.
CVE-2025-68161log4j-core seul.Faux positif sur log4j-api.
CVE-2026-49844log4j-api seul.Faux positif sur log4j-core.
CVE-2021-44228 (Log4Shell)log4j-core.Posée sur log4j-core seulement.
Deux archives, les mêmes indices, un seul CPE : les CVE de toute la famille retombent sur chaque fichier.

Le même mécanisme joue ailleurs dans le rapport, plus discrètement. jackson-databind 2.9.8 se voit attribuer trois CPE, dont jackson-core et jackson-modules-java8, deux bibliothèques que cette archive ne contient pas. spring-web hérite d'un cpe:2.3:a:web_project:web:5.2.0, un produit nommé "web" déduit du seul nom d'artefact. Dans cette exécution, ce CPE parasite n'a rien ajouté ; à la prochaine synchronisation de la NVD, rien ne garantit qu'il reste muet. Voilà pourquoi une équipe qui adopte Dependency-Check finit toujours par maintenir un fichier de suppression : une liste XML de correspondances exclues, nommément, avec une justification datée.

xml
<?xml version="1.0" encoding="UTF-8"?>
<suppressions xmlns="https://jeremylong.github.io/DependencyCheck/dependency-suppression.1.4.xsd">
  <suppress>
    <notes>CVE-2026-34479 vise le pont log4j-1.2-api, absent du projet.
           Vérifié sur la NVD le 2026-09-02.</notes>
    <packageUrl regex="true">^pkg:maven/org\.apache\.logging\.log4j/log4j-(api|core)@.*$</packageUrl>
    <cve>CVE-2026-34479</cve>
  </suppress>
</suppressions>

Le fichier se passe avec --suppression et se versionne avec le projet. Chaque entrée est une décision humaine : on a lu la CVE, on a vérifié le composant, on exclut. Supprimer sans lire, c'est fabriquer un faux négatif à la main, ce qui est pire que le faux positif de départ.

Alertes, vraies vulnérabilités, failles atteignables

Mettons les deux rapports côte à côte, avec trois définitions strictes. Une alerte, c'est ce que l'outil affiche. Une vraie vulnérabilité, c'est une CVE distincte, rattachée à un composant réellement présent dans une version réellement concernée. Une faille atteignable, c'est du code vulnérable qu'un attaquant peut faire exécuter avec une entrée qu'il contrôle, dans cette application précise. Les deux projets de test n'ont aucun code applicatif : personne n'appelle ces bibliothèques. La troisième colonne ne peut donc pas être un chiffre, et c'est la leçon : aucun des deux outils ne sait la remplir, sur aucun projet.

Mesurenpm audit (Node, 93 paquets)Dependency-Check (Java, 10 archives)
Alertes affichées14 paquets vulnérables (3 low, 7 high, 4 critical)133 entrées de vulnérabilité
Avis ou CVE distincts71 avis GHSA, dont 25 sur le seul axios103 CVE distinctes
Doublons0, une entrée par paquet30, une entrée par fichier (24 Spring, 6 Log4j)
Rattachement erronéAucun : identité lue dans le lockfile4 entrées fausses sur 6 examinées côté Log4j
Exploitées dans la nature (KEV)Non signalé par l'outil3 CVE : Log4Shell, CVE-2021-45046, Spring4Shell
Atteignables iciIndéterminé : l'outil ne regarde pas les appelsIndéterminé : idem

Lecture ligne par ligne. npm audit affiche petit et compte juste : quatorze paquets, sans doublon ni erreur d'identité, mais il tait le volume réel (71 avis) et ne dit pas lesquels sont exploités dans la nature. Dependency-Check affiche gros et compte large : 133 lignes dont trente redondantes et quelques-unes mal posées, mais son analyseur KEV isole en une colonne les trois CVE qui méritent un ticket ce soir. Et même celles-là ont des conditions : Spring4Shell exige Java 9 ou plus, un déploiement WAR sous Tomcat, et ne touche pas un exécutable Spring Boot ; Log4Shell suppose qu'une chaîne attaquant-contrôlée atteigne un appel de log avec les lookups actifs. La CVE-2016-1000027 de spring-web, critique 9.8, porte même une réserve de l'éditeur, pour qui désérialiser des données non fiables n'est pas un usage prévu. Trancher ces cas, c'est ce qui distingue un scan de vulnérabilité d'un test d'intrusion, et sur du code source c'est le terrain du pentest whitebox.

La distinction n'est pas cosmétique. Une gestion des vulnérabilités qui traite les trois nombres comme un seul finit noyée : elle ouvre un ticket par ligne de rapport, sans hiérarchie. Le tri utile commence par séparer ce qui est présent de ce qui est appelé, et ce qui est appelé de ce qui est appelé avec une entrée hostile. Le score CVSS d'une CVE mesure sa gravité intrinsèque, pas son atteignabilité dans votre contexte : une critique 10.0 sur un composant jamais chargé pèse moins, en pratique, qu'une élevée 7.5 sur le chemin d'authentification.

Où le SCA s'arrête, et ce qui prend le relais

Les deux outils partagent la même frontière. Ils lisent des versions et les comparent à des bases de vulnérabilités connues. Ils ne savent rien de la façon dont votre application appelle ces composants, ni des failles de votre propre code. Un SCA ne verra jamais une injection SQL que vous avez écrite, ni un contrôle d'accès manquant : ce n'est pas son travail, c'est celui de l'analyse statique et dynamique. Et aucun scanner ne confirme qu'une dépendance vulnérable est réellement exploitable : ça, c'est le rôle d'un opérateur.

En pratique, un SCA se pose tôt et se relance souvent. On le branche dans la chaîne d'intégration pour bloquer l'introduction d'une dépendance critique, et on complète l'inventaire par un SBOM, la nomenclature logicielle qui liste noir sur blanc chaque composant et sa version. Le jour où une nouvelle Log4Shell tombe, c'est ce SBOM qui répond en minutes à la seule question qui compte : est-ce que ce composant est chez moi, et où ? Les outils SCA commerciaux ajoutent surtout de l'ergonomie, du suivi et de l'analyse d'atteignabilité par-dessus ce socle ; le moteur de comparaison, lui, reste le même que celui de ces deux scanners libres.

Reste un angle mort que ni l'un ni l'autre ne couvre : le paquet malveillant. Un scanner SCA compare vos versions à des failles déclarées ; il ne détecte pas un paquet légitime en apparence mais piégé, comme dans une attaque par typosquatting. La composition analysée est saine du point de vue des CVE connues, et pourtant compromise. C'est une raison de plus de ne pas confondre "aucune alerte SCA" avec "chaîne d'approvisionnement sûre".

À retenir

  • npm audit est précis mais mono-écosystème : il lit l'arbre résolu par npm, connaît la version exacte et ne devine jamais. Sur notre projet Node : 14 paquets vulnérables sur 93, dont 4 critiques.
  • OWASP Dependency-Check est large mais indirect : il reconstruit l'identité d'un composant par indices, fabrique un CPE et interroge la NVD. D'où sa couverture multi-langages et ses faux positifs.
  • Le CPE matching produit des faux positifs : à vérifier un par un et à exclure via un fichier de suppression, jamais à corriger à l'aveugle.
  • Alerte, vraie vulnérabilité et faille atteignable sont trois nombres différents : aucun des deux outils ne fait d'analyse d'atteignabilité. Le tri reste humain.
  • Le SCA est une brique, pas un audit : il ignore votre code, la logique métier et les paquets malveillants. Il se complète d'un SBOM, d'analyse de code et d'un test d'intrusion.

Ces deux scanners libres suffisent à savoir ce que vous embarquez. Ils ne disent pas ce qui est exploitable chez vous. Le module SCA d'own2pwn croise vos composants avec les vulnérabilités connues en continu, et quand il faut trancher entre une alerte et une faille réelle atteignable sur votre périmètre, un pentester le confirme en conditions réelles.

Questions fréquentes sur OWASP Dependency-Check et npm audit

Quelle différence entre OWASP Dependency-Check et npm audit ?

npm audit compare l'arbre de dépendances installé à la base d'avis GitHub, par plage de versions exacte : il sait précisément quel paquet et quelle version sont concernés. OWASP Dependency-Check devine le composant à partir d'indices extraits du fichier (nom, éditeur, version), fabrique un identifiant CPE et interroge la base NVD. La première méthode est précise mais limitée à l'écosystème npm ; la seconde couvre Java, .NET, Python et d'autres, au prix de faux positifs quand l'indice est mal deviné.

Qu'est-ce que le software composition analysis (SCA) ?

Le software composition analysis est l'analyse des composants tiers d'une application pour y trouver des vulnérabilités connues. Un outil SCA inventorie les bibliothèques embarquées, identifie leur version, puis croise cet inventaire avec des bases publiques comme la NVD ou les avis GitHub. C'est un contrôle de vulnérabilités connues sur du code que vous n'avez pas écrit, à distinguer de l'analyse statique de votre propre code.

OWASP Dependency-Check produit-il des faux positifs ?

Oui, et c'est structurel. L'outil identifie un composant à partir d'indices textuels et fabrique un identifiant CPE ; quand deux bibliothèques partagent un nom ou qu'un éditeur est ambigu, il peut rattacher une CVE à un composant qui n'est pas concerné. Ces faux positifs se gèrent avec un fichier de suppression, qui exclut nommément une correspondance CPE erronée après vérification manuelle.

Une alerte de scanner SCA veut-elle dire que je suis exploitable ?

Non. Une alerte signale qu'une version vulnérable est présente dans l'arbre, pas que le code vulnérable est appelé avec une entrée contrôlable par un attaquant. Ni npm audit ni Dependency-Check ne font d'analyse d'atteignabilité : ils comptent des composants, pas des chemins d'exécution exploitables. Le tri entre alerte et faille réellement atteignable reste un travail humain, éventuellement confirmé par un test d'intrusion.

Comment corriger les alertes remontées par npm audit ?

npm audit fix met à jour les paquets vers une version corrigée compatible avec les plages déclarées. npm audit fix --force applique aussi les correctifs qui cassent la compatibilité sémantique, ce qui peut monter une dépendance directe d'une version majeure : à réserver aux mises à jour testées. Pour une dépendance transitive vulnérable, la correction dépend du parent, qui doit lui-même publier une version fixée.

Un scanner SCA remplace-t-il un test d'intrusion ?

Non. Un scanner SCA couvre uniquement les vulnérabilités connues des composants tiers, par comparaison de versions. Il ne teste pas la logique métier, l'authentification, les contrôles d'accès ni les failles propres à votre code, et ne confirme pas qu'une dépendance vulnérable est réellement atteignable. C'est une brique de la chaîne, complémentaire de l'analyse statique et d'un test d'intrusion mené par un opérateur.

Veille sécurité

La suite, une fois par mois

Ce qui bouge vraiment sur la surface d'attaque externe, NIS2 et la sécurité applicative, écrit par le pentester qui signe ces articles. Un envoi par mois, désinscription en un clic.

Votre adresse ne sert qu'à cet envoi. Voir la politique de confidentialité.