Aller au contenu principal
Site en cours de refonte — quelques pages peuvent bouger ou évoluer.
13 mars 2026

Supply-chain : npm audit / composer audit ne te protègent pas

Un audit « clean » ne prouve rien contre un package malveillant. Si tu veux dormir, il faut des garde-fous côté install, CI/CD et gestion des tokens.

10 min de lecture
16 vues
réactions
Partager :
Supply-chain : npm audit / composer audit ne te protègent pas (et quoi mettre à la place)

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.

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.

Sources

Cet article vous a plu ?

Commentaires

Laisser un commentaire

Entre 10 et 2000 caractères

Les commentaires sont modérés avant publication.

Aucun commentaire pour le moment.

Soyez le premier à donner votre avis !