Une défense simple contre les attaques de chaîne d'approvisionnement npm
Chaque fois que vous exécutez npm install, vous accordez votre confiance à du code provenant de centaines de paquets que vous n’avez jamais lus. C’est précisément cette confiance que les attaquants exploitent. Lors du ver Shai-Hulud de septembre 2025, des scripts malveillants exécutés à l’installation ont silencieusement collecté des identifiants et republié des paquets compromis à grande échelle, le tout avant même que l’invite de commande ne réapparaisse. La compromission d’Axios en mars 2026 a suivi le même schéma : des versions malveillantes (1.14.1 et 0.30.4) tiraient une dépendance dotée d’un hook postinstall exécutant une charge utile d’accès à distance.
Vous n’avez pas besoin d’un outillage lourd pour réduire cette exposition. Un seul changement de configuration suffit à contrer directement le vecteur d’attaque le plus courant.
Points clés à retenir
- Les scripts de cycle de vie npm (
preinstall,install,postinstall,prepare) s’exécutent automatiquement avec les pleins privilèges de l’utilisateur, ce qui en fait la voie de moindre résistance pour les attaques de chaîne d’approvisionnement. - Définir
ignore-scripts=truedans.npmrcbloque le vecteur d’attaque le plus répandu avec une seule ligne de configuration. - Les paquets contenant des binaires natifs comme
sharp,bcryptetsqlite3peuvent toujours être compilés sélectivement à l’aide denpm rebuildaprès une installation sans scripts. - Une vérification en CI sur
package-lock.jsondétectant les nouvelles entrées"hasInstallScript": truepermet de signaler les dépendances nécessitant un examen avant leur fusion. - Associer
ignore-scripts=trueàmin-release-age=7ajoute un délai qui aide à éviter les versions de paquets fraîchement compromises.
Pourquoi les scripts de cycle de vie constituent le risque central
Lorsque vous installez un paquet npm, Node exécute automatiquement tous les scripts définis dans son package.json — preinstall, install, postinstall et prepare. Ces hooks s’exécutent avec les pleins privilèges de votre utilisateur, donnant accès à ~/.ssh, ~/.aws/credentials, aux jetons de ~/.npmrc et à toutes les variables d’environnement de votre shell.
Aucune invite de confirmation. Aucun bac à sable. Juste une exécution silencieuse.
Dans l’attaque Axios, une dépendance transitive malveillante nommée plain-crypto-js utilisait un hook postinstall pour déposer un cheval de Troie d’accès à distance. Dans l’attaque par usurpation de la CLI Bitwarden, un hook preinstall amorçait une importante charge utile obfusquée qui collectait les identifiants cloud et tentait de se propager via l’accès de publication npm de la victime.
La sécurité des scripts de cycle de vie est cruciale, car elle représente la voie d’attaque la moins contraignante de tout le paysage de la sécurité des dépendances JavaScript.
La défense fondamentale : npm ignore-scripts
Ajoutez une ligne au fichier .npmrc de votre projet :
ignore-scripts=true
Cela indique à npm de sauter tous les scripts de cycle de vie lors de l’installation. Un hook postinstall malveillant ne s’exécutera tout simplement pas.
Pour une installation ponctuelle sans modifier .npmrc :
npm install --ignore-scripts
Avertissement important : ce n’est pas une solution complète. L’attaque Bitwarden enregistrait un binaire malveillant via le champ bin du package.json, ce qui signifie que la charge utile s’exécutait quand même lorsque l’utilisateur invoquait la commande bw — même avec --ignore-scripts activé. Aucun drapeau unique n’élimine tous les risques.
Ce qui se casse et comment le gérer
Certains paquets légitimes nécessitent des scripts de cycle de vie pour compiler des binaires natifs. Les plus courants sont sharp, bcrypt, sqlite3, canvas et puppeteer (qui télécharge Chromium).
Vérifiez si votre projet utilise l’un d’entre eux :
npm ls --all | grep -E "(sharp|bcrypt|sqlite3|canvas|puppeteer|node-gyp)"
Si vous obtenez des correspondances, recompilez uniquement ces paquets spécifiques après l’installation :
npm install --ignore-scripts
npm rebuild sharp --ignore-scripts=false
npm rebuild bcrypt --ignore-scripts=false
Cette approche maintient les scripts désactivés globalement tout en autorisant sélectivement la compilation pour les paquets que vous avez explicitement examinés.
Discover how at OpenReplay.com.
Détecter les nouveaux scripts d’installation avant leur exécution
Plutôt que de réagir après coup, signalez les nouvelles dépendances comportant des scripts d’installation avant leur fusion. Ajoutez cette vérification à votre pipeline CI :
# Detect newly added packages with install scripts in PRs
if git diff origin/main -- package-lock.json | \
grep -E '^\+\s*"hasInstallScript": true' > /dev/null; then
echo "⚠️ New install script detected—manual review required"
exit 1
else
echo "✅ No new install scripts"
fi
Dans l’attaque Axios, plain-crypto-js était une toute nouvelle dépendance dotée d’un hook postinstall. Cette vérification l’aurait signalée avant la fusion de la PR.
Pour plus de détails sur la manière dont npm enregistre les scripts d’installation dans les lockfiles, consultez la documentation officielle de package-lock.json.
Ce que ces défenses ne couvrent pas
Soyons honnêtes quant aux limites :
- Les lockfiles n’empêchent pas les attaques si un développeur a exécuté
npm installpendant la fenêtre de compromission et a empoisonné le lockfile avant que vous ne le détectiez. npm auditne détecte que les paquets malveillants connus — les attaques inédites n’apparaîtront pas dans sa base de données.- L’A2F sur les comptes npm ne protège pas les consommateurs contre un paquet légitimement publié par un mainteneur compromis.
--ignore-scriptsne bloque pas le code malveillant intégré dans le JavaScript d’exécution du paquet lui-même.
Une approche en couches est utile. Associez ignore-scripts=true à min-release-age=7 dans .npmrc (nécessite npm v11.10.0 ou ultérieur) pour éviter d’installer les paquets publiés au cours de la dernière semaine — la fenêtre pendant laquelle la plupart des attaques sont actives et non détectées. Ce paramètre est documenté dans la documentation de configuration npm sous min-release-age.
Commencez ici
Ajoutez ceci à votre .npmrc dès aujourd’hui :
ignore-scripts=true
min-release-age=7
Ajoutez ensuite la vérification CI pour les nouvelles entrées hasInstallScript sur chaque PR touchant package-lock.json. Ces deux changements répondent au schéma d’attaque utilisé dans les récentes attaques de chaîne d’approvisionnement npm — sans nécessiter de nouveaux outils, de services payants ou de modifications significatives du flux de travail.
Conclusion
Vous ne pouvez pas lire chaque ligne de chaque paquet dont vous dépendez, mais vous pouvez fortement limiter ce que ces paquets sont autorisés à faire pendant l’installation. Désactiver les scripts de cycle de vie par défaut, recompiler uniquement les paquets natifs en lesquels vous avez confiance et signaler les nouvelles entrées hasInstallScript en CI permet, ensemble, de neutraliser le vecteur d’attaque le plus exploité de l’écosystème npm. Aucune de ces mesures n’exige de nouveaux fournisseurs ou de budget — juste quelques lignes dans .npmrc et une seule vérification CI. Adoptez-les dès aujourd’hui, et la prochaine attaque de type Shai-Hulud aura un impact bien moindre sur votre projet.
FAQ
Cela peut casser les paquets qui compilent des binaires natifs lors de l'installation, tels que sharp, bcrypt, sqlite3, canvas et puppeteer. Après avoir exécuté npm install avec les scripts désactivés, utilisez npm rebuild suivi du nom du paquet avec --ignore-scripts=false pour compiler uniquement les paquets de confiance que vous avez examinés. La plupart des dépendances purement JavaScript fonctionneront sans aucun changement.
Non. Il bloque le vecteur le plus courant — les hooks de cycle de vie comme postinstall — mais n'arrêtera pas le code malveillant dans le JavaScript d'exécution d'un paquet ni un binaire malveillant enregistré via le champ bin. L'attaque par usurpation de la CLI Bitwarden a contourné ignore-scripts de cette manière. Combinez-le avec min-release-age, l'examen des lockfiles et l'audit des dépendances pour une défense en couches.
Le paramètre min-release-age nécessite npm version 11.10.0 ou ultérieure. Vérifiez votre version avec npm --version et mettez à niveau si nécessaire en utilisant npm install -g npm@latest. Vérifiez que le paramètre est actif en utilisant npm config get min-release-age après l'avoir configuré.
Validez votre fichier .npmrc avec ignore-scripts=true dans le dépôt afin que la CI hérite automatiquement du paramètre. Pour les builds qui nécessitent réellement une compilation native, ajoutez des commandes npm rebuild explicites (avec --ignore-scripts=false) pour les paquets spécifiques requis. Cela maintient la valeur par défaut sécurisée tout en documentant exactement quels paquets sont autorisés à exécuter du code d'installation.
Gain control over your UX
See how users are using your site as if you were sitting next to them, learn and iterate faster with OpenReplay. — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.