À propos de l'article du MagIT « NPM : une nouvelle campagne malveillante souligne une vulnérabilité systémique ».
NPM expliqué simplement
Quand on développe une application web moderne en JavaScript ou TypeScript, on ne réécrit jamais tout depuis zéro. On assemble des briques logicielles déjà écrites par d'autres : un module pour parser des dates, un autre pour valider des emails, un troisième pour discuter avec une base de données. Ces briques s'appellent des paquets, et la place de marché centrale qui les distribue s'appelle npm (Node Package Manager).
Concrètement, dans un projet, on déclare la liste des paquets nécessaires dans un fichier package.json. On lance la commande npm install, et l'outil télécharge automatiquement les paquets demandés… ainsi que tous les paquets dont ces paquets ont eux-mêmes besoin. Un projet « simple » se retrouve souvent à dépendre de plusieurs centaines, voire plusieurs milliers, de paquets en cascade. C'est ce qu'on appelle l'arbre des dépendances.
Le registre npm héberge aujourd'hui plus de 2,5 millions de paquets. C'est à la fois sa force — un écosystème colossal, une productivité décuplée — et sa faiblesse : la confiance accordée à chaque maillon de la chaîne est implicite, et chaque maillon devient une porte d'entrée potentielle.
La faille : ce qui s'est passé
L'épisode décrit par LeMagIT n'est pas un bug logiciel classique. C'est ce qu'on appelle une attaque sur la chaîne d'approvisionnement logicielle (supply chain attack) : au lieu d'attaquer directement la cible finale, l'attaquant compromet un fournisseur en amont, et laisse la mise à jour légitime faire son travail de propagation.
Le scénario reconstitué se déroule en plusieurs temps.
1. Compromission d'un paquet de confiance. Les attaquants sont parvenus à pousser du code malveillant dans des paquets npm largement utilisés, notamment via le détournement du pipeline d'intégration continue de projets connus comme @bitwarden/cli et l'écosystème Checkmarx. L'astuce n'est pas de publier un faux paquet : c'est de modifier un vrai paquet en exploitant les GitHub Actions — les robots qui construisent et publient automatiquement les nouvelles versions.
2. Vol de secrets à l'installation. Une fois installé sur la machine d'un développeur ou dans un environnement de build, le code malveillant scanne l'environnement à la recherche de variables sensibles : GITHUB_TOKEN, NPM_TOKEN, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY. Tout ce qui traîne dans les variables d'environnement, les fichiers .env, les configurations cloud.
3. Auto-propagation. C'est là que l'attaque devient virale. Avec les jetons npm volés, le maliciel se reconnecte au registre npm, récupère la liste des paquets publiés par la victime, et publie automatiquement des versions piégées de ces paquets. Chaque développeur compromis devient un super-propagateur. Socket a identifié une quarantaine de paquets infectés en cascade lors d'une seule vague.
4. Persistance. Sur les postes touchés, le malware installe un script systemd pour survivre aux redémarrages, et, si nécessaire, exfiltre les données volées dans un dépôt GitHub public créé pour l'occasion.
Le résultat : un binaire signé, publié sous un nom officiel, à jour, qui passe tous les contrôles de surface — et qui contamine simultanément le poste du développeur et les serveurs de production.
Pourquoi c'est « systémique »
Le terme employé par LeMagIT est juste. Ce n'est pas un bug isolé, c'est une propriété structurelle de l'écosystème.
La confiance est transitive. On fait confiance à express, qui fait confiance à body-parser, qui fait confiance à qs, etc. Compromettre un nœud profond et populaire suffit à toucher des millions de projets.
La publication est ouverte. N'importe qui peut publier un paquet. Les contrôles existent (provenance, 2FA pour les mainteneurs populaires) mais restent surtout a posteriori.
Les scripts d'installation s'exécutent automatiquement. Un paquet npm peut déclarer un postinstall qui lance du code arbitraire au moment de npm install. C'est pratique, mais c'est aussi un cheval de Troie idéal.
Les jetons d'API sont partout. Le poste du développeur, les runners CI/CD, les serveurs : tous manipulent des secrets en clair dans des variables d'environnement. Un malware qui s'exécute dans le build n'a même pas besoin d'escalader ses privilèges.
Les versions sont mutables sur fenêtre courte. Un paquet peut être republié dans les 72 heures suivant sa publication, et un npm unpublish peut retirer une version d'un jour à l'autre.
Aucun de ces points n'est un défaut technique réparable par un patch. Ce sont des choix d'architecture, vieux de plus de dix ans, qui ont accompagné l'explosion de l'écosystème.
Y a-t-il des alternatives ?
La question est légitime, mais la réponse honnête est : pas vraiment, et pour de bonnes raisons.
Les gestionnaires de paquets alternatifs
pnpm, yarn et bun sont des gestionnaires différents, mais ils tirent leurs paquets du même registre npm. Migrer de npm install à pnpm install ne change rien à la surface d'attaque : ce sont les mêmes paquets, le même registre, les mêmes mainteneurs.
Cela dit, certains apportent des garde-fous utiles :
pnpma introduit l'optionminimumReleaseAge, qui refuse d'installer un paquet publié il y a moins de N jours. Une vague d'attaque dure typiquement quelques heures avant détection : attendre 72 heures avant d'installer une nouvelle version élimine la fenêtre dangereuse.pnpmimpose un consentement explicite pour les scriptspostinstall, là où npm les exécute par défaut.bunetyarnproposent des lockfiles stricts (--frozen-lockfile) qui garantissent que ce qui est installé en CI correspond exactement à ce qui a été testé.
Les registres alternatifs
JSR (JavaScript Registry), lancé par les créateurs de Deno, est le seul vrai nouveau registre crédible. Il a été conçu en tirant les leçons des problèmes de npm : TypeScript natif, modules ECMAScript par défaut, pas de scripts d'install, scoring qualité automatique, compatible avec tous les runtimes (Node, Deno, Bun). Mais JSR est complémentaire, pas un remplaçant : il héberge des milliers de paquets, pas des millions. Pour la majorité des dépendances, on continuera de passer par npm.
Les registres privés — Verdaccio, GitHub Packages, JFrog Artifactory, Sonatype Nexus — ne remplacent pas npm non plus. Ils servent de proxy filtrant : on continue de récupérer les paquets publics, mais à travers un cache d'entreprise où l'on peut bloquer une version, exiger une signature, refuser un mainteneur, ou interdire les paquets publiés depuis moins de X jours. C'est probablement le meilleur compromis disponible aujourd'hui pour une organisation.
Le verdict
Abandonner npm en 2026 reviendrait à abandonner JavaScript. La valeur de l'écosystème (2,5 millions de paquets) est trop importante pour qu'on en sorte. Le problème ne se résoudra pas par un changement d'outil ; il se résoudra par un changement de pratiques.
Changer les pratiques : ce qui doit devenir réflexe
L'enseignement de cette campagne, et des précédentes (Shai-Hulud, TeamPCP, l'attaque Trivy/KICS), tient en une phrase : la confiance par défaut est morte. Il faut traiter chaque dépendance comme du code hostile par défaut, et le pipeline CI/CD comme une zone de production.
Au niveau du poste de développement
- Activer l'option
minimumReleaseAge(ou équivalent) pour différer l'installation des paquets fraîchement publiés. - Désactiver les scripts
postinstallpar défaut, et n'autoriser que ceux explicitement validés. - Ne jamais stocker de jetons en clair dans
~/.npmrcou les variables d'environnement persistantes. Préférer un gestionnaire de secrets (1Password CLI,pass,keyring). - Utiliser des comptes npm séparés pour la publication, avec 2FA matérielle obligatoire.
Au niveau du dépôt
- Verrouiller systématiquement les dépendances (
package-lock.json,pnpm-lock.yaml,yarn.lock) et installer en mode strict (npm ci,pnpm install --frozen-lockfile). - Mettre en place un audit automatique des dépendances à chaque PR (Socket, Snyk, GitHub Dependabot,
npm audit). - Publier ses propres paquets avec provenance npm (signature liée au pipeline GitHub Actions), pour que les consommateurs puissent vérifier l'origine.
- Tenir à jour un SBOM (Software Bill of Materials) pour savoir exactement ce qui tourne en production.
Au niveau du CI/CD
C'est probablement le chantier le plus important.
- Cloisonner les jetons. Un jeton de publication npm ne doit jamais coexister avec un jeton AWS dans la même étape de pipeline. Un secret par étape, durée de vie minimale, scope minimal.
- Préférer les jetons à courte durée de vie (OIDC entre GitHub Actions et le cloud) plutôt que des clés statiques.
- Auditer les GitHub Actions tierces. Une action
uses: foo/bar@v1est l'équivalent d'uncurl | bash. Épingler par hash SHA (@a1b2c3...), pas par tag mutable. - Restreindre les permissions du
GITHUB_TOKENau strict minimum (permissions: read-allpar défaut,writeponctuel et justifié). - Surveiller le comportement réseau des runners : un build qui contacte un domaine inconnu doit lever une alerte.
Au niveau de l'organisation
- Mettre en place un registre proxy (Verdaccio, Nexus, Artifactory) avec liste blanche/noire de paquets, et l'imposer comme unique source pour tous les projets.
- Définir une politique de dependency governance : qui peut introduire une nouvelle dépendance, sous quelles conditions, avec quel niveau d'audit.
- Prévoir un playbook de révocation : que faire dans l'heure qui suit la détection d'un paquet compromis (rotation de tous les jetons npm/GitHub/cloud, audit des artefacts publiés, communication).
En résumé
NPM n'est pas cassé, il est tel qu'il a été conçu : ouvert, automatique, transitif. Ce qui a changé, c'est la valeur que les attaquants peuvent en extraire — secrets cloud, jetons CI/CD, accès aux pipelines — et la sophistication des campagnes, qui exploitent désormais l'auto-propagation pour atteindre une échelle virale.
Aucune alternative ne supprime le problème, parce que le problème n'est pas npm : c'est l'idée qu'on puisse exécuter en production du code écrit par des inconnus sans jamais le regarder. Le rôle du DevOps en 2026, c'est de bâtir l'infrastructure qui rend cette inspection systématique, économique et inévitable — registres proxy, lockfiles stricts, jetons éphémères, audits continus, isolation des étapes de build.
On ne fera pas confiance à moins de gens. On exigera juste que chaque maillon prouve, à chaque exécution, qu'il est bien celui qu'il prétend être.
Commentaires
Aucun commentaire pour l'instant. Soyez le premier !
Laisser un commentaire