pnpm face à Shai-Hulud : repenser la sécurité des dépendances applicatives
Comment un simple npm install peut-il devenir une porte d'entrée pour exfiltrer les secrets d'une organisation, et comment s'en prémunir sans renoncer à l'écosystème JavaScript ?
Un npm install exécute automatiquement les scripts des paquets installés et donne accès à un graphe de milliers de dépendances transitives. Il suffit qu'un seul paquet soit compromis pour exfiltrer des secrets.
pnpm réduit cette surface d'attaque en isolant les dépendances, en bloquant les scripts d'installation par défaut et en imposant un délai de maturité avant installation.
En 2025, le ver informatique Shai-Hulud a compromis des paquets npm totalisant plusieurs millions de téléchargements hebdomadaires, exfiltrant des secrets et se répliquant automatiquement dans d'autres paquets. L'incident illustre une faille structurelle de l'écosystème JavaScript : la confiance implicite envers plus de 2,5 millions de paquets.
Cette analyse propose un tour d'horizon des mécanismes exploités par Shai-Hulud et des réponses apportées par l'outil pnpm :
- isolation des dépendances,
- blocage des scripts d'installation
- délais de maturité avant installation.
Shai-Hulud : anatomie d'une attaque supply chain
Une attaque supply chain compromet une cible non pas via son code source, mais via l'une de ses dépendancestierces : du code malveillant est injecté dans un outil de confiance, puis distribué automatiquement à tous les projets qui en dépendent.
L'écosystème JavaScript y est particulièrement exposé, avec un registre npm de plus de 2,5 millions de paquets et des projets dépendant régulièrement de centaines de modules transitifs.
Shai-Hulud, nommé d'après le ver des sables de l'univers Dune, est un ver auto-réplicant ciblant le dépôt de paquets npm.
Son fonctionnement :
- Une campagne de phishing usurpe l'identité du registre npm, pousse les mainteneurs à mettre à jour leur authentification multi-facteurs, et compromet leurs identifiants.
- Du code malveillant est injecté dans leurs dépôts. La charge utile se déclenche au prochain npm install d'un projet consommateur du dépôt.
- Le malware parcourt l'environnement à la recherche de secrets (tokens, variables d'environnement, clés d'API), les exfiltre vers un endpoint contrôlé par l'attaquant, puis les publie dans un dépôt GitHub public créé sous le compte de la victime.
- Si la victime contribue à d'autres paquets npm, le malware se propage automatiquement via les identifiants volés.
En novembre 2025, une seconde itération plus agressive est apparue, utilisant le script preinstall (exécution plus précoce) et tentant de supprimer le répertoire home de la victime en cas d'échec d'exfiltration.
Parmi les victimes documentées : @ctrl/tinycolor, une bibliothèque cumulant plusieurs millions de téléchargements hebdomadaires.
npm : vulnérable par design
Les dépendances fantômes, une surface d'attaque élargie
npm, le principal gestionnaire de dépendances de l'écosystème JavaScript, a fait des choix architecturaux qui simplifient la vie des développeurs et développeuses mais ouvrent une large surface d'attaque.
Pour éviter la duplication massive des paquets, npm aplatit le dossier node_modules : les dépendances transitives sont remontées (hoisted) au plus haut niveau possible de l'arborescence. Par exemple, sur un projet utilisant express, qui déclare path-to-regexp comme dépendance :
mon_app/
└── node_modules/
├── express/
├── …
└── path-to-regexp/Cette structure rend possible l'import d'un module non déclaré dans le package.json du projet, dès lors qu'il est présent au moins une fois dans l'arbre de dépendances. On parle de dépendances fantômes.
const regexp = require('path-to-regexp');La surface d'attaque effective d'un projet devient alors l'intégralité du graphe de dépendances transitives, et non plus seulement ses dépendances directes. Par ailleurs, npm reconstruit cet arbre à chaque installation, ce qui pose des problèmes de performance connus depuis plusieurs années.
Les scripts d'installation, un vecteur d'exécution arbitraire
npm permet également aux paquets d'exécuter du code arbitraire au moment de l'installation, via des scripts preinstall, install et postinstall. Initialement conçus pour des usages légitimes (compilation, génération de configuration), ces hooks s'exécutent automatiquement, de manière transparente, avec les privilèges de l'utilisateur courant. C'est exactement le vecteur exploité par Shai-Hulud pour déployer sa charge utile.
Les défenses face à ces attaques sont pourtant limitées parmi les gestionnaires de paquets populaires :
- npm propose un flag --ignore-scripts, mais il bloque tous les scripts, légitimes inclus.
- Depuis la version 11.10 (février 2026), npm propose une option min-release-age définissant un délai minimum avant installation d'un paquet, mais son adoption sera lente et ne sera pas universelle.
- Pareillement, à partir de sa version 12.0 (juillet 2026), npm bloquera les scripts preinstall et postinstall par défaut. Mais cette option, mettra du temps à être adoptée et ne règlera pas les problèmes de structure du dossier node_modules.
- Yarn Classic (v1), un concurrent, ne propose aucune protection face à ces problématiques.
- À partir de la v2, Yarn offre une architecture plus sécurisée et un blocage des scripts via enableScripts, mais ces protections ne sont pas actives par défaut, et cette version reste peu adoptée comparée à la v1.
Même si les philosophies évoluent enfin, les principaux gestionnaires de dépendances font donc confiance par défaut à l'intégralité du registre et à ce qu'il peut exécuter. Il devient nécessaire de chercher une alternative intégrant la sécurité dès sa conception (shift left).
pnpm : la sécurité par conception
Une structure node_modules isolée
pnpm (performant npm) répond point par point aux faiblesses de npm, tout en utilisant le même registre central que ses concurrents.
pnpm élimine les dépendances fantômes en n'exposant à chaque paquet que ce qu'il a explicitement déclaré, bloque les scripts d'installation par défaut depuis la v10 (janvier 2025), et permet via minimumReleaseAge de refuser tout paquet publié trop récemment - les trois leviers exploités par les attaques supply chain.
pnpm maintient un store global sur la machine, où chaque fichier de chaque version de chaque paquet est stocké une seule fois. Lors de l'installation d'un projet, pnpm crée une structure en deux niveaux dans node_modules :
1. Un virtual store dans node_modules/.pnpm/ contenant toutes les dépendances (directes et transitives), sous forme de liens système vers le store global.
2. À la racine de node_modules/, uniquement les dépendances directes, sous forme de liens symboliques vers le virtual store.
mon_app/
└── node_modules/
├── express/ (lien symbolique vers le virtual store)
└── .pnpm/
└── [email protected]/
└── node_modules/
└── express/ (lien système vers le store global)Cette structure offre trois avantages forts :
- Composée uniquement de liens, l'installation est très rapide, surtout si les dépendances sont déjà présentes dans le store global.
- Les fichiers étant mutualisés, ils ne sont plus dupliqués dans chaque projet : on gagne de l'espace disque.
- Toute tentative d'import d'un paquet non déclaré comme dépendance directe échoue. Les dépendances fantômes n'existent plus : chaque paquet ne voit que ce qu'il a explicitement déclaré.
Blocage des scripts et maturité des paquets
De plus, depuis pnpm v10 (janvier 2025), les scripts d'installation sont bloqués par défaut. Autoriser un paquet à exécuter ses scripts demande une déclaration explicite, ce qui rend visible quels paquets exécutent du code à l'installation.
S’y ajoute l'option minimumReleaseAge, qui permet de bloquer l'installation de paquets publiés trop récemment : une protection essentielle face aux attaques supply chain qui misent sur une propagation rapide.
Parmi d'autres avantages, pnpm offre un support natif des monorepos, et la commande pnpm patch qui permet de modifier temporairement le code d'une dépendance directement dans le projet : utile pour appliquer immédiatement un correctif de sécurité sans attendre une mise à jour officielle.
Le tableau ci-dessous récapitule les différences entre les gestionnaires de paquet :
| Critère | npm | Yarn v1 | Yarn v2+ | pnpm |
|---|---|---|---|---|
| Scripts bloqués par défaut | ❌ (Jusqu'à v12) | ❌ | ❌ | ✅ |
| Dépendances fantômes | ✅ | ✅ | ✅ (Par défaut) | ❌ |
| Isolation des dépendances | ❌ | ❌ | ❌ (Par défaut) | ✅ |
| Délai de maturité avant installation | ✅ (v11.10+) | ❌ | ✅ (v4.10+) | ✅ (v10.16+) |
Migrer vers pnpm : à quel coût ?
Les actions à prévoir
Pour beaucoup de projets, la migration est un remplacement quasi transparent de npm :
rm -rf node_modules package-lock.json
pnpm install
pnpm run build
pnpm run testpnpm générera son propre pnpm-lock.yaml, à commiter en remplacement de l 'ancien package-lock.json.
Quelques actions manuelles restent à prévoir :
- Chemins « en dur » vers node_modules : à mettre à jour pour pointer vers le virtual store, ou à refactorer.
- Dépendances fantômes : tout import non déclaré lèvera une erreur 'Cannot find module xxxx', à corriger en ajoutant le paquet dans le package.json.
- Scripts d'installation bloqués : les paquets nécessitant un preinstall ou postinstall doivent être autorisés explicitement. pnpm affiche un avertissement pour chaque paquet bloqué.
- Pipelines CI/CD : remplacer npm ci par pnpm install --frozen-lockfile, pour une installation conforme au pnpm-lock.yaml.
Sur un projet conséquent chez SQLI (application Angular en production, 1500 dépendances transitives, plusieurs centaines de tests unitaires), la migration a pris environ deux jours, rédaction d 'un retour d'expérience inclus. Avec de l'habitude et une documentation existante, ce temps peut être réduit.
Les limites de pnpm
Mais pnpm n'est pas une solution miracle. Plusieurs vecteurs d'attaque restent ouverts :
- Les paquets dont les scripts sont autorisés s'exécutent sans contrôle, avec les droits de l'utilisateur courant.
- Le code malveillant exécuté au runtime n'est pas filtré.
- Les attaques de typosquatting (publier un paquet vérolé au nom proche d'un paquet connu) restent possibles.
Adopter pnpm ne dispense donc pas des bonnes pratiques classiques : audits réguliers des dépendances, épinglage des versions, revue manuelle des mises à jour avant intégration. Ces pratiques sécurisent un projet indépendamment du gestionnaire de paquets.
Shai-Hulud n'a pas contourné un mécanisme de sécurité, il a exploité une faille structurelle, la confiance dans un écosystème de 2,5 millions de paquets, héritée d'une époque où npm n'en comptait que quelques milliers.
pnpm ne protège pas contre toutes les menaces : ni contre un mainteneur dont les identifiants sont volés, ni contre un paquet légitime qui introduit une régression. Il apporte des protections structurelles qui réduisent considérablement la surface d'attaque, avec des gains de performance notables et un coût d'adoption modéré, même sur des projets existants.
pnpm est donc à considérer sérieusement sur les nouveaux projets, et à évaluer pour les projets existants, en particulier ceux exposés à des risques liés aux dépendances, c'est-à-dire pratiquement tous. Plusieurs projets open-source majeurs ont d'ailleurs déjà migré, comme Vue, Vite ou Material UI.
Pour en savoir plus
- pnpm – Documentation officielle
- npm – Documentation officielle
- CISA – Widespread Supply Chain Compromise Impacting npm Ecosystem