Le pire piège avec npm audit et composer audit, ce n’est pas qu’ils soient inutiles. C’est qu’ils rassurent. Un joli vert, zéro vulnérabilité, et tu crois que ta supply-chain est sous contrôle. Sauf que les attaques les plus sales aujourd’hui ne passent pas par une CVE. Elles passent par un package qui fait “juste” une chose en plus au moment où tu installes, buildes ou publies.
Je garde audit dans mes pipelines. Mais je le traite comme un détecteur de fumée basique, pas comme un système d’extinction. Le vrai filet de sécurité, c’est une hygiène minimale, répétable, et orientée “ce qui peut réellement arriver” sur une stack web JS/PHP.
Pourquoi “audit” te laisse passer des packages malveillants
npm audit et composer audit sont fondamentalement des outils base de données → matching. Ils signalent ce qui est connu, documenté, publié comme advisory, souvent avec une CVE ou au moins un identifiant dans une base. Ça marche très bien pour des failles classiques : prototype pollution documentée, RCE dans une version précise, bypass auth dans un package populaire.
Mais un package qui vole ton .env, ton ~/.npmrc ou qui injecte un backdoor discret ne va pas forcément déclencher quoi que ce soit. Pas parce que c’est “trop sophistiqué”. Juste parce que personne ne l’a encore classé dans une base. Et même quand l’info existe quelque part, le temps que ça remonte proprement, tu peux avoir déjà installé la version piégée, l’avoir packagée en image, et l’avoir déployée.
Autre angle mort : l’attaque n’est pas toujours dans le code “exécuté par ton app”. Elle peut être dans ce qui s’exécute pendant ton build, dans ta CI, dans un script d’install, dans un plugin Composer, dans un job GitHub Actions trop permissif. audit ne comprend pas ton contexte. Il voit des versions.
Sponsorisé par Le Scribouillard
Besoin de contenu optimisé SEO ?
Utilisez la meilleure plateforme française de création de contenu assistée par IA ! Et générez des articles pour moins de 1€ !
Les signaux qui comptent vraiment (et qu’un audit ne voit pas)
Sur npm, le grand classique, ce sont les lifecycle scripts. Un preinstall ou un postinstall qui part lire des fichiers locaux, inspecter ton home, récupérer des variables d’environnement, puis faire une requête sortante. Ça n’a rien de magique. C’est juste du Node qui s’exécute au pire moment : quand tu as des secrets à portée (tokens, configs, env CI) et que tu es concentré sur “ça compile”.
Ce que je surveille en priorité, c’est tout ce qui ressemble à une lecture opportuniste de fichiers sensibles (~/.npmrc, .env, clés SSH, config cloud), tout ce qui fait du réseau “sans raison produit”, et tout ce qui est obfusqué. L’obfuscation n’est pas une preuve. Mais en supply-chain, c’est rarement un bon signe, surtout sur des packages censés être des utilitaires.
Côté PHP/Packagist, il y a un piège spécifique aux frameworks : l’exécution “à l’amorçage”. Entre l’autoload, l’auto-discovery de providers et certains hooks de plugins, tu peux te retrouver à exécuter du code bien avant d’avoir “utilisé” la lib. Une dépendance qui se fait passer pour un helper Laravel et qui injecte un provider, c’est exactement le genre d’attaque qui ne ressemble pas à une CVE. Ça ressemble à un package “normal” qui fait une chose en plus, au mauvais endroit.
Ce que je mets à la place : le playbook “hygiène minimale”
Il n’y a pas une solution miracle. Il y a un empilement de garde-fous. L’objectif n’est pas d’atteindre un fantasme de sécurité parfaite. L’objectif, c’est de rendre l’attaque coûteuse, visible, et difficile à propager, surtout dans la CI.
Désactiver ce qui exécute du code à l’installation (par défaut)
Sur npm, si tu ne fais rien, tu acceptes que n’importe quel package exécute des scripts pendant npm install. C’est pratique quand tu compiles du native, c’est une autoroute pour l’exfiltration de secrets si tu installes dans un environnement qui en contient. La posture raisonnable, c’est scripts off par défaut dans les environnements sensibles (CI, build d’images), et scripts on seulement là où tu as explicitement besoin.
; .npmrc (CI ou build)
ignore-scripts=true
fund=false
audit=false
Oui, j’ai mis audit=false ici. Pas parce que je n’en veux pas, mais parce que je préfère que l’audit soit un job dédié, lisible, avec sa propre politique d’échec. Mélanger “installer” et “auditer” produit souvent un pipeline confus, et les devs finissent par ignorer le bruit.
Sur Composer, même logique. Tu peux couper l’exécution de scripts, et surtout contrôler les plugins. Les plugins Composer sont puissants. Donc dangereux. Composer 2 a justement introduit une politique d’allowlist de plugins : utilise-la. Un projet qui laisse “n’importe quel plugin” s’exécuter parce que ça passait comme ça depuis des années, c’est un cadeau.
{
"config": {
"allow-plugins": {
"php-http/discovery": true,
"dealerdirect/phpcodesniffer-composer-installer": true
}
}
}
Le but n’est pas d’avoir cette allowlist parfaite dès le premier jour. Le but est de créer un point de contrôle. Chaque nouveau plugin devient une discussion, pas une surprise.
Rendre les builds hermétiques (et arrêter de “builder avec des secrets”)
Beaucoup d’équipes se font piéger parce qu’elles buildent dans une CI où tout est disponible : tokens npm, clés de déploiement, accès à un registry privé, credentials cloud. Le package malveillant n’a même pas besoin d’être “utilisé”. Il lui suffit d’être installé au bon moment.
Une règle simple qui change tout : le job qui installe des dépendances et compile ne doit pas avoir accès à des secrets de publication ou de prod. Si tu as besoin de publier, fais-le dans un job séparé, qui récupère les artefacts buildés, et qui a des permissions minimales et temporaires. Ça évite aussi les effets collatéraux : une compromission d’un cache ou d’un runner ne devient pas une compromission de ton compte npm.
Durcir la CI/CD : permissions minimales, cache traité comme non fiable
Les incidents récents le montrent bien : l’attaque supply-chain moderne adore les “effets de bord” de la CI. Les workflows trop permissifs, les bots qui ingèrent des données externes, les caches partagés, et les jobs qui tournent avec des tokens puissants, c’est un terrain de jeu.
Sur GitHub Actions, je considère que permissions: write-all est un anti-pattern. Tu veux un workflow qui annonce clairement ce qu’il peut faire. Tu veux aussi éviter de laisser un token npm de publication traîner dans un job qui tourne sur des événements exposés (issues, PR d’un fork, commentaires). Et si tu utilises des automatisations “IA” qui lisent des issues, traite ça comme une interface non fiable. Une issue GitHub, c’est de l’input attaquant. Point.
Le cache mérite une phrase à lui tout seul. Si un attaquant peut influencer ce qui entre dans ton cache, il peut influencer ce qui en sort. Donc soit tu verrouilles la clé et la source de vérité, soit tu acceptes que ton cache soit un vecteur. Dans les faits, ça veut dire : clés de cache strictes, séparation par branches/contexts, et refus d’utiliser un cache “écrit” par des jobs non fiables.
Arrêter d’upgrader à l’aveugle : lockfile, pinning et diff de tarball
Le “^ partout” et les upgrades automatiques sans lecture, ça marche tant que tout le monde est honnête. En supply-chain, tu dois partir du principe inverse. La baseline, c’est le lockfile commit et revu, et des upgrades qui sont des changements contrôlés.
Et quand tu upgrades, je ne veux pas seulement “lire le changelog”. Je veux comparer ce qui est publié. Le réflexe utile, c’est le diff de tarball ou le diff entre versions sur le code réellement distribué. Parce que le repo GitHub et le package publié ne sont pas toujours identiques. Et parce que l’attaque peut être injectée dans l’artefact publié, pas dans le repo que tu regardes vite fait.
Si tu n’as pas de process pour ça, commence petit : sur les dépendances sensibles (celles qui tournent dans la CI, celles qui touchent au réseau, celles qui chargent du code, celles qui font de la crypto), une review d’upgrade sans diff, c’est non.
Provenance et attestations : utile, mais seulement si tu t’en sers
La provenance (au sens attestation de build, signature, traçabilité) est une bonne direction. Elle ne remplace pas le reste. Elle répond à une question précise : “cet artefact vient-il bien de là où il prétend venir, et a-t-il été produit par un workflow attendu ?”. Ça ne répond pas à “le code est-il sain ?”. Mais c’est déjà énorme, parce que beaucoup d’attaques passent par l’usurpation, la compromission d’un compte mainteneur, ou une chaîne de build modifiée.
Si tu peux activer des mécanismes de provenance sur tes propres packages internes, fais-le. Pour l’open source, adopte-les progressivement, en commençant par tes dépendances critiques. Et surtout, ne transforme pas ça en tampon “secure” dans ta tête. La provenance réduit une classe d’attaques. Elle n’annule pas le besoin de garde-fous à l’installation et en CI.
Politique d’équipe : tokens, registries, et confiance explicite
La supply-chain, ce n’est pas juste “des packages”. C’est aussi des identités et des accès. Un token npm de publish qui vit six mois dans un secret CI et qui a des droits trop larges, c’est une bombe à retardement. Même chose côté Packagist/Composer si tu as des accès privés ou des registries internes.
Mon approche est simple : tokens courts, scopes minimaux, rotation régulière, et séparation nette entre “installer” et “publier”. Et si ton équipe utilise des registries privés ou des scopes, tu peux faire un pas de plus avec une confiance explicite : seules certaines orgs/scopes sont autorisés, le reste est bloqué. Ça ne protège pas contre tout, mais ça coupe net une partie des attaques opportunistes (typosquatting, confusion de registry).
Dernier point, très terre-à-terre : arrêtez de considérer qu’un package “petit” est moins risqué. C’est souvent l’inverse. Un micro-helper au nom générique, publié par un compte inconnu, qui s’insère dans un projet Laravel ou un outillage Node, c’est exactement la taille idéale pour passer sous le radar.
Mon avis (sec) : garde audit, mais enlève-lui le droit de te rassurer
npm audit et composer audit ont leur place. Ils attrapent des vulnérabilités connues, et c’est déjà très bien. Le problème commence quand “audit est clean” devient un argument de sécurité. Ce n’est pas un argument. C’est juste l’absence d’un signal dans une base.
Si tu veux une posture réaliste en 2026, fais simple et ferme : scripts et plugins sous contrôle, CI hermétique, permissions minimales, tokens courts, upgrades revues avec diff d’artefact quand c’est sensible. Ce n’est pas de la parano. C’est juste de l’ingénierie.