- Une attaque de la chaîne d’approvisionnement a touché plus de 40 packages de l’écosystème NPM, dont le populaire @ctrl/tinycolor, avec injection d’un malware auto-propagateur capable d’infecter en cascade les secrets des environnements de développement jusqu’aux identifiants CI/CD. Les versions compromises ont été retirées de npm
- La charge utile exécute de manière asynchrone un bundle Webpack (
bundle.js, ~3.6MB) pendant l’installation npm, puis procède à une collecte étendue d’identifiants via les variables d’environnement, le système de fichiers et les SDK cloud - La logique malveillante utilise NpmModule.updatePackage pour patcher et republier de force d’autres packages, provoquant une propagation en cascade, et injecte un workflow shai-hulud dans GitHub Actions pour exfiltrer les secrets d’organisation via toJSON(secrets)
- Les données collectées sont exfiltrées via la création du dépôt GitHub public
Shai-Hulud, en se faisant passer pour une activité de développement légitime, ce qui augmente fortement la furtivité - L’opération agit discrètement en visant les tokens et les points de terminaison de métadonnées de AWS/GCP/Azure/NPM/GitHub, ainsi qu’en recherchant des secrets sur la base de TruffleHog
- Il est demandé de retirer immédiatement les packages, nettoyer les dépôts, remplacer l’ensemble des identifiants, ainsi que de vérifier les journaux CloudTrail/GCP Audit, bloquer les webhooks et mettre en place des politiques de protection de branche / Secret Scanning / cooldown
Packages affectés
- 195 packages/versions au total ont été signalés, notamment @ctrl/tinycolor(4.1.1, 4.1.2), de nombreux packages de l’espace de noms @ctrl/, la famille de modules @crowdstrike/, ainsi que ngx-bootstrap/ngx-toastr/ng2-file-upload/ngx-color à travers l’écosystème Angular/Web UI, la pile mobile @nativescript-community/ et @nstudio/, la toolchain sciences de la vie teselagen/, ainsi que ember-*, koa2-swagger-ui, pm2-gelf-json, wdio-web-reporter
- Pour chaque package, il faut se référer au tableau de l’article source pour les versions exactes et croiser précisément leur présence dans vos projets
- Exemples :
@ctrl/ngx-emoji-mart 9.2.1, 9.2.2,@ctrl/qbittorrent 9.7.1, 9.7.2,ngx-bootstrap 18.1.4, 19.0.3–20.0.5,ng2-file-upload 7.0.2–9.0.1
- Exemples :
Actions immédiates requises
Identifier et supprimer les packages compromis
- Vérifier la présence de packages infectés dans le projet :
npm ls @ctrl/tinycolor - Désinstaller immédiatement les packages compromis :
npm uninstall @ctrl/tinycolor - Rechercher localement les traces connues via le hash de
bundle.js:sha256sum | grep 46faab8a...
Nettoyer les dépôts infectés
- Supprimer le workflow GitHub Actions malveillant : retirer
.github/workflows/shai-hulud-workflow.yml - Détecter et supprimer la branche distante
shai-hulud:git ls-remote ... | grep shai-huludpuisgit push origin --delete shai-hulud
Faire tourner immédiatement tous les identifiants
- Remplacement complet requis pour les tokens NPM, GitHub PAT / secrets GitHub Actions, clés SSH, identifiants AWS/GCP/Azure, chaînes de connexion DB, tokens tiers et secrets CI/CD
- Rotation complète nécessaire, y compris pour les éléments stockés dans AWS Secrets Manager / GCP Secret Manager
Auditer l’infrastructure cloud pour détecter une compromission
- AWS : dans CloudTrail, vérifier les horaires et motifs d’appels à
BatchGetSecretValue,ListSecrets,GetSecretValue, et contrôler via le IAM Credential Report la création ou l’usage anormal de clés - GCP : dans les Audit Logs, vérifier les accès à Secret Manager et la présence éventuelle d’événements CreateServiceAccountKey
1 commentaires
Commentaire Hacker News
En tant qu’utilisateur de paquets hébergés sur npm, j’ai le sentiment qu’il n’est pas réaliste de surveiller moi-même toutes les dépendances, y compris les dépendances de mes dépendances, et comme je ne suis pas non plus expert TypeScript/JavaScript, je pense que je ne repérerais pas facilement le code malveillant dissimulé par un attaquant ; ces derniers temps, je réfléchis à une manière de mettre à jour en « mode différé », c’est-à-dire n’accepter que des versions qui ont déjà un certain âge au lieu de toujours prendre la toute dernière, en partant du principe que si un paquet est exposé publiquement depuis environ 6 semaines, il y a de fortes chances qu’un malware ait été découvert ; ce n’est pas une méthode parfaite, et j’aimerais qu’il existe un outil permettant de faire une exception pour appliquer immédiatement la dernière mise à jour en cas de problème de sécurité
La méthode est d’ailleurs mentionnée directement dans l’article : il existe une fonctionnalité appelée NPM Package Cooldown Check, qui fait automatiquement échouer le build lorsqu’une version de paquet publiée dans le délai défini par l’organisation (2 jours par défaut) est ajoutée à une pull request ; comme la majorité des attaques sur la supply chain sont détectées dans les 24 heures, même un très court délai d’attente peut réduire l’exposition au risque
Comme il est difficile d’inspecter l’ensemble des dépendances, j’aimerais défendre l’idée qu’il faut réduire autant que possible leur nombre et n’utiliser que des paquets connus et dignes de confiance ; à moins d’être dans un environnement suffisamment contrôlé pour faire confiance à tous les auteurs, conserver une certaine dose de « not-invented-here » reste au fond un choix raisonnable
J’ai l’habitude de fixer explicitement les versions dans
package.jsonet d’utilisernpm cipour n’installer que celles indiquées danspackage-lock.json; j’exécutenpm auditen CI pour être alerté si des vulnérabilités apparaissent dans les paquets ; de cette manière, les paquets sont quasiment « gelés », et l’ancienneté des paquets elle-même réduit la probabilité d’infectionPour ma part, je pousse encore plus loin et je ne mets à jour les dépendances que lorsqu’un bug affecte réellement mon environnement d’utilisation ; même en présence d’une faille de sécurité, si elle ne m’impacte pas, je passe mon tour ; la plupart des développeurs mettent leurs dépendances à jour bien trop souvent sans nécessité, alors qu’il vaudrait mieux ne le faire qu’en cas de vrai besoin ; si les mises à jour sont fréquentes ou compliquées, alors je n’utilise tout simplement pas ce paquet, ou je le « gèle » selon mes critères
Avec l’outil
uvde Python, on peut limiter les mises à jour de manière similaire ; par exemple, une commande commeuv lock --exclude-newer $(date --iso -d "2 days ago")permet d’exclure les versions publiées dans les 2 derniers joursCe problème existe parce que les nouveaux paquets ou les nouvelles versions ne sont pas surveillés ; la meilleure approche serait de séparer, comme Debian, une distribution stable ne recevant que des correctifs de sécurité et de bugs, et des distributions testing/unstable surveillées par les mainteneurs des paquets ; tous ceux qui travaillent sur des dépôts centralisés de paquets open source (NPM, Python, Rust, etc.) sont confrontés au même problème
Il y a un problème de culture chez les développeurs ; avoir des centaines de dépendances (transitives) et les mettre à jour automatiquement sans réflexion fait partie du problème ; exposer ses environnements de build et d’exécution à autant de code tiers implique une responsabilité
Les distributions ressentent elles aussi de plus en plus le poids du nombre de paquets à maintenir ; c’est d’ailleurs en grande partie pour cela que les écosystèmes propres à chaque langage (CPAN, Maven, RubyGems, etc.) se sont développés ; les distributions Linux seules ont du mal à fournir toutes les applications souhaitées par les utilisateurs, d’où l’apparition de multiples canaux comme freshmeat, linuxbrew, flatpak, PPA, etc. ; je doute qu’une communauté quelconque ait les moyens de surveiller et supporter les multiples branches de tant de bibliothèques diverses
En tant que développeur Debian, il devient de plus en plus difficile de repérer les changements réels en amont à cause du « bruit » croissant (en particulier les simples changements de style ou les mises à jour d’outillage) ; j’aimerais que ce type de changements soit évité, sauf s’il s’agit de refactorings nécessitant réellement une revue humaine, de corrections de bugs, d’ajouts de fonctionnalités, ou de résultats d’outils permettant d’identifier du code potentiellement problématique
En Rust, il existe un système appelé cargo vet, auquel participent des entreprises comme Google et Mozilla pour partager et vérifier automatiquement des paquets
Je pense qu’il existe des moyens de garder une certaine décentralisation tout en ajoutant quelques garde-fous ; par exemple, imposer pour les paquets d’une certaine taille une double approbation depuis deux comptes avec 2FA, ou n’autoriser l’upload sur npm des paquets populaires que via un système de builds reproductibles ; cela ne reviendrait pas à abandonner complètement la décentralisation, seulement à demander un petit effort supplémentaire sur les gros projets
À cause des attaques répétées sur la supply chain ces derniers temps, je réfléchis beaucoup plus sérieusement au server-side rendering (sans JavaScript) ; grâce à HTMX, j’ai réalisé qu’on peut aller vraiment très loin sans JavaScript, et l’application pourrait même être plus rapide et plus stable de cette façon
Je voudrais souligner que l’environnement JS traditionnel est en réalité le sandbox le plus sûr ; depuis presque 30 ans, du code JS non fiable s’exécute sur des milliards d’appareils, et les attaques de grande ampleur réussies contre les moteurs de navigateur se comptent sur les doigts d’une main ; en revanche, l’environnement NodeJS et npm a besoin d’une refonte complète du point de vue de la sécurité ; des épisodes comme leftpad viennent de cette culture qui consiste à publier sur npm même de simples snippets de code
Je trouve étrange que ce type d’attaque soit automatiquement ramené à un problème propre à un environnement particulier (JavaScript) ; en réalité, le plus gros problème est peut-être que même les mesures de sécurité déjà disponibles sur npm ne sont pas du tout appliquées dans d’autres environnements (PyPI, Crates, etc.)
Le vendoring peut réduire l’exposition, mais ce n’est pas une solution au problème de fond ; si NPM prenait vraiment la sécurité au sérieux, il faudrait rendre obligatoires la 2FA pour la publication, un scan préalable des paquets, et même une signature par clé matérielle ; semver ou CRC ne suffisent pas ; tout cela devrait être intégré nativement au système de gestion de paquets
En réalité, ce n’est pas un problème propre à JavaScript, mais au fait que les développeurs ne surveillent pas suffisamment les nouvelles dépendances qu’ils ajoutent ; cela pourrait tout aussi bien s’appliquer à d’autres écosystèmes comme Rust ou Go
Tous les langages qui dépendent fortement d’un package manager et disposent d’une bibliothèque standard pauvre sont vulnérables de la même manière ; à long terme, je me dis qu’il faudrait peut-être revenir à davantage de JavaScript vanilla ; Rust présente la même forte dépendance aux paquets ; au contraire, Go est un cas exemplaire sur ce point
Je pense qu’il faut un système léger permettant de suivre du code signé sur les commits/releases avec des clés de confiance, puis de l’installer et de le vérifier ; il existe déjà un mécanisme de provenance npm avec sigstore, mais cela ne semble pas encore largement utilisé et paraît limité pour l’instant à la validation de l’éditeur
Dès 2016, cette vulnérabilité avait déjà été signalée à NPM (avis CERT), mais la réponse de NPM avait été WAI (working as intended)
Pour ceux qui ne savent pas ce que signifie WAI : c’est généralement l’abréviation de « working as intended »
Même en l’absence totale de script postinstall, il semble qu’il suffise au final d’importer le module pendant le processus de build, au démarrage du serveur ou lors des tests pour exécuter le malware ; de toute façon, après
npm install, il y a forcément un moment où quelque chose sera effectivement exécuté...Cela me rappelle un commentaire vu ici lors de l’affaire left-pad : il était question d’un mainteneur npm renommé avec 600 paquets npm et 1 200 lignes de code JavaScript ; un exemple que j’aimerais citer est esbuild, qui n’a quasiment pas de dépendances externes et utilise uniquement la bibliothèque standard de Go
Même parmi les autres projets dits « next-gen », quand on regarde la chaîne de dépendances, biomejs et swc en ont aussi relativement peu ; mais si l’on examine le code source Rust original, biomejs et swc ont en fait de nombreuses dépendances ; je m’attends à ce que l’écosystème cargo suive le même chemin à mesure que ce type de projets se répand ; si quelqu’un connaît un gros projet écrit avec une discipline stricte comme esbuild, je suis preneur
L’une des raisons pour lesquelles je suis passé à Go est justement la tendance des bibliothèques purego ; en général, elles ne dépendent que de la bibliothèque standard et de
golang.org/x, et peuvent être compilées sans CGO, ce qui leur donne une excellente portabilité ;go mod vendorpermet de gérer le risque à court terme, mais ce n’est pas une solution de fond ; Go reste lui aussi vulnérable, faute de vérification des paquets de bout en bout (signatures / vérification de clés, etc.) ; l’attention se concentre beaucoup sur l’infrastructure CI/CD, mais si l’on pouvait build et déployer sans transmettre de clés de signature, la sécurité pourrait aussi y gagner ; je pense que les package managers devraient encourager les signatures GPG, et que les commits git devraient eux aussi être signés avant diffusionLe cas d’eslint est particulièrement frustrant, si l’on regarde son graphe de dépendances ; si les mainteneurs ne font pas de la réduction des dépendances une priorité, il ne reste plus qu’à migrer vers une autre solution comme oxlint
La solution consiste à implémenter soi-même les fonctionnalités simples et à réduire les dépendances externes ; en pratique, rien que cela permet souvent de supprimer les deux tiers des dépendances ; surtout pour quelque chose de simple comme left-pad, le faire soi-même et le garder sous contrôle avec de petites unités et des tests n’ajoute pas une charge de maintenance énorme ; il faut éliminer sans hésiter les dépendances inutiles
Ce qui apparaît dans le
Cargo.tomlracine d’un projet Rust concerne tout l’espace de travail ; les dépendances réelles de chaque crate sont bien plus modestes ; il faut regarder plus en profondeur pour comprendre la vraie structure des dépendancesL’inconvénient, c’est qu’il faut maintenant savoir lire aussi du Golang pour auditer des projets JavaScript ; et comme un
node install.jsest de nouveau exécuté en post-installation, au final il ne reste qu’à faire une confiance totale ou à lire tout le codeJe n’arrive pas à croire que npm exécute encore par défaut les scripts
postinstallde toutes les dépendances ; Pnpm ou Bun ne les exécutent que si elles figurent dans une liste d’autorisation, et Composer n’exécute tout simplement pas de scripts de cycle de vie pour les dépendances ; compte tenu du risque que représentent les paquets de dépendance dans les environnements de build ou de développement, cette approche me paraît plus sûreJe me demande pourquoi ce genre d’attaque à grande échelle semble beaucoup plus rare avec d’autres package managers (par exemple le
build.rsde Rust, Python, Java, etc.) ; pourtant, au-delà depostinstall, c’est en principe possible dans quasiment tous les écosystèmes, mais les incidents semblent se concentrer surtout autour de npmJ’ai vu que Pnpm a changé sa valeur par défaut pour bloquer les scripts ; je serais curieux de connaître les retours de la communauté (sur l’expérience utilisateur quand il faut autoriser des scripts, sur l’abus de la commande allow, etc.), et la communauté du packaging Python discute aussi de sujets similaires autour des variantes de wheels ; j’aimerais m’inspirer de l’expérience d’autres écosystèmes
Cette attaque s’est désormais propagée à plus de 180 paquets ; voir le blog d’Aikido Security
Je me demande qui a été le premier à découvrir cette attaque ; il est intéressant de voir que les blogs attribuent le mérite de manière différente ; Aikido dit « nous avons découvert une attaque à grande échelle », et Socket, Ox, Safety, Phoenix, Semgrep, etc. la présentent chacun à leur façon
Je suis Mackenzie d’Aikido ; la première personne à avoir signalé l’affaire est le développeur Daniel Pereira, qui l’a transmise à Socket, et Socket a été le premier à analyser les 40 premiers paquets et le malware ; ensuite, Aikido a identifié 147 paquets supplémentaires ainsi que le paquet Crowdstrike ; en réalité, c’est Step qui a compris en premier que le malware était un ver auto-propagateur ; il est intéressant de voir plusieurs organisations jouer indépendamment des rôles différents
Il semble que plusieurs développeurs l’aient découvert à peu près au même moment, et Step comme Socket citent chacun des personnes différentes ; au final, les éditeurs de sécurité du secteur l’ont détecté chacun par leurs propres moyens, qu’il s’agisse d’analyse de code par IA (Socket, Aikido) ou de monitoring de pipeline eBPF (Step)
Si autant d’éditeurs l’ont détecté indépendamment, on peut se demander pourquoi ils ne partagent pas directement cette technologie avec npm pour bloquer l’enregistrement même des paquets malveillants ; sinon, ils ne pourraient plus vendre de système d’alerte précoce, ce qui explique peut-être qu’ils ne le fassent pas
L’article de l’OP cite textuellement : « @franky47 a découvert ce phénomène puis l’a signalé rapidement à la communauté via une issue GitHub »
Je trouve le nom donné par l’attaquant, « Shai Hulud », assez bien trouvé : donner à un véritable malware de type ver le nom d’un ver géant ; même le
bundle.jsprincipal fait 3,6 Mo, et jusqu’aux variantes du malware, tout devient énorme de façon très npmJ’ai le pressentiment qu’un jour une attaque de supply chain finira aussi par en attirer accidentellement une autre
Les malwares aussi suivent la loi de Moore : le virus tequila de 1991 faisait 2,6 Ko, aujourd’hui on est en plusieurs Mo