# Un ver dans npm, et pourquoi go get n'exécute aucun script

> En 2026, deux vers ont infecté des centaines de paquets npm, avec des signatures de provenance parfaitement valides. Leur point commun : du code exécuté pendant l'installation. Go a fait un autre pari. Il ne le rend pas invulnérable, il déplace le moment où le code tourne.


*Ou : Comment un fichier package.json a appris à se reproduire*

Le 4 août 2026, installer une bibliothèque de cache aussi banale que keyv suffisait à exécuter un voleur d'identifiants, avant même qu'une seule ligne de ton code ne l'importe. Trois mois plus tôt, quarante-deux paquets de TanStack avaient subi le même sort. Les deux attaques ont un point commun qui devrait inquiéter tout le monde : les versions piégées portaient des signatures de provenance parfaitement valides. Elles en ont un second, plus banal et plus décisif : le code malveillant s'exécutait pendant l'installation, au moment où personne ne regarde. Go a fait sur ce point un pari différent, il y a des années. Ce pari ne le rend pas invulnérable. Il déplace le moment où le code d'une dépendance s'exécute, et ce déplacement change beaucoup de choses.

## Attaque de la chaîne d'approvisionnement npm : ce que le modèle des modules Go change
{.subtitle}

### Deux vagues, une même porte d'entrée

La première vague touche TanStack le 11 mai 2026. Entre 19 h 20 et 19 h 26 UTC, l'attaquant publie 84 versions malveillantes réparties sur 42 paquets `@tanstack/*`[^1]. Il n'a volé aucun mot de passe : il a enchaîné un workflow GitHub Actions qui exécutait le code des forks, un empoisonnement du cache de ce workflow, puis l'extraction, dans la mémoire de la machine qui exécutait le workflow, du jeton qui sert à publier sur npm. La seconde vague, baptisée CHAINDROP par Elastic, commence le 4 août à 9 h 02 UTC, quand l'identité GitHub compromise du mainteneur de keyv pousse un commit directement sur la branche principale[^2].

| | TanStack, 11 mai 2026 | keyv et cacheable, 4 août 2026 |
|---|---|---|
| Entrée | Workflow GitHub Actions détourné | Compte GitHub du mainteneur |
| Déclencheur à l'installation | Script `prepare` d'une dépendance optionnelle | Script `preinstall` : `node setup.mjs` |
| Provenance des versions piégées | Valide | Valide |
| Ampleur | 84 versions, 42 paquets | Plus de 400 paquets, par propagation |

La suite tient du ver au sens propre. Une fois installé, le code fouille la machine à la recherche de jetons : npm, GitHub, fournisseurs cloud. Avec un jeton npm qui autorise la publication, il republie chaque paquet de sa victime, infecté à son tour. Elastic décrit la règle : dès qu'un jeton exploitable est trouvé, « the worm enumerates every package the victim has publish rights to ». Aikido comptait au moins 444 paquets compromis le lendemain, Elastic plus de 400[^2].

### La provenance prouvait la bonne chose

La provenance npm atteste qu'un paquet a été construit par la chaîne de publication officielle du dépôt. C'est exactement ce qui s'est passé. L'attaquant avait poussé son code dans le dépôt, et la chaîne de publication a fidèlement construit, signé et publié ce qu'on lui donnait. Socket le résume pour TanStack : « Do not trust Sigstore provenance badges alone as a security signal »[^1]. Pour keyv, l'alerte déposée le jour même sur le dépôt va plus loin : « Republishing packages with valid provenance is a capability of this payload »[^3]. Republier avec une provenance valide n'était pas un accident, c'était une fonction.

**DevOps Dave :** Je n'installe que des paquets avec une provenance vérifiée.

**Security Sarah :** Vérifiée par qui ?

**DevOps Dave :** Par GitHub. Le paquet vient bien du dépôt officiel.

**Security Sarah :** Le ver aussi.

La provenance répond à la question « d'où vient ce paquet ? ». Elle ne répond pas à « que fait ce paquet ? ». Et sous npm, la réponse à la seconde question arrive au pire moment possible : pendant `npm install`, sur ton poste ou dans ton intégration continue, là où traînent justement les jetons que le ver cherche.

### Ce que Go refuse de faire au téléchargement

Le billet de l'équipe Go sur la chaîne d'approvisionnement pose le principe en une phrase : « neither fetching nor building code will let that code execute, even if it is untrusted and malicious »[^4]. Pas de script d'installation, pas de point d'accroche, rien. J'ai voulu le voir plutôt que le croire. J'ai fabriqué un module piégé, servi par un proxy local, avec Go 1.27.1.

```go
package evil

//go:generate sh -c "touch MARK-generate"

import "os"

// init runs when a program that imports this package starts. Not before.
func init() {
	os.WriteFile("MARK-init", []byte("ran"), 0o644)
}

func Hello() string { return "hello" }
```

`go get`, `go mod download`, `go build` et `go vet` sur un programme qui l'importe : aucun fichier marqueur créé. Et si l'on tente de lancer la directive de génération de la dépendance, Go refuse tout net :

```text
$ go generate example.com/evil
go: not generating in packages in dependency modules
```

Le marqueur n'apparaît qu'au lancement du programme compilé, quand `init()` s'exécute. C'est tout le changement : le code de la dépendance tourne quand tu exécutes ton programme, pas quand tu le télécharges.

### La somme de contrôle qui ne change jamais d'avis

Le second pilier est le fichier `go.sum`, qui garde l'empreinte de chaque version utilisée, adossé à une base publique, `sum.golang.org`. La référence des modules la décrit comme un « Transparent Log (or "Merkle Tree") », un journal que des auditeurs indépendants peuvent vérifier[^5]. Pour l'éprouver, j'ai monté un proxy menteur qui sert une version altérée d'une ligne du vrai module `github.com/google/uuid` v1.6.0.

```text
go: github.com/google/uuid@v1.6.0: verifying module: checksum mismatch
	downloaded: h1:w4aREtnuHZ/R9lL3DzHUwfPch3QPAbZXsvP/kc7IdVI=
	sum.golang.org: h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=

SECURITY ERROR
This download does NOT match the one reported by the checksum server.
```

Même refus quand une archive change après avoir été enregistrée dans `go.sum`. Ajoute la sélection de version minimale : Go prend la version que demande le `go.mod`, pas la dernière parue, et depuis Go 1.16 les commandes de construction refusent de modifier `go.mod` en silence[^5]. Une version piégée publiée cette nuit n'entre pas dans ta compilation sans qu'un humain la demande.

### Ce que Go ne bouche pas

Ce modèle a un angle mort, et il est cruel. La même référence précise que la base de sommes garantit que le contenu d'une version ne change pas d'un jour à l'autre, « even if the module's author subsequently alters the tags in their repository ». C'est ce qui a démasqué mon proxy menteur. C'est aussi ce qui a protégé un module piégé pendant plus de trois ans. En novembre 2021, `github.com/boltdb-go/bolt`, un faux BoltDB au nom presque identique, publie une version 1.3.1 munie d'une porte dérobée. Le miroir de Go la met en cache, puis l'auteur réécrit l'étiquette sur un code propre. Le dépôt GitHub paraît sain, le miroir continue de servir la version piégée. Elle n'a été signalée qu'en février 2025[^6].

Le deuxième angle mort tient au mot « exécution ». Go n'exécute rien au téléchargement, mais `go test ./...` lance les `init()` de toutes tes dépendances (le marqueur de mon module piégé apparaît aussi là), et `go tool` exécute les outils déclarés par la directive `tool` de Go 1.24. Dans une intégration continue qui lance les tests avec des jetons de publication dans l'environnement, une dépendance piégée trouve exactement ce que cherchaient les vers npm. Go déplace le moment du risque, il ne le supprime pas.

Et npm bouge aussi. Depuis pnpm 10, publié le 7 janvier 2025, les scripts d'installation des dépendances ne s'exécutent plus par défaut[^7].

### Ce que je fais concrètement

Je garde `go.sum` dans le dépôt et je ne désactive jamais `GOSUMDB` pour des modules publics. Je lance `govulncheck` dans l'intégration continue : il signale en priorité les failles des fonctions que ton code appelle vraiment, et la base qu'il consulte recense aussi des modules malveillants, dont ce faux BoltDB[^8]. Je relis lettre par lettre le chemin de toute nouvelle dépendance. Et les tests tournent sans jeton de publication : un `init()` ne peut pas voler ce qui n'est pas là. Pour un éditeur de logiciel, l'enjeu n'est plus seulement technique : une faille activement exploitée dans un produit peut désormais déclencher [le signalement en 24 heures du Cyber Resilience Act](/blog/cyber-resilience-act-signalement-24-heures/). Et ces attaques par les dépendances ne visent pas que les bibliothèques : [les extensions des agents d'IA ont suivi le même chemin](/blog/openclaw-anatomie-attaques/).

Le ver CHAINDROP n'a rien inventé. Il a profité d'une seule ligne de `package.json`, exécutée par des millions de machines qui ne lui avaient rien demandé. Go n'a pas cette ligne. Il te reste à ne pas la réinventer dans ton intégration continue.

---

[^1]: Le [post-mortem de TanStack](https://tanstack.com/blog/npm-supply-chain-compromise-postmortem) détaille la fenêtre de 19 h 20 à 19 h 26 UTC, les 84 versions sur 42 paquets et la chaîne d'attaque, et précise que les publications utilisaient une authentification de publication de confiance valide, liée au workflow de publication du dépôt. [Socket](https://socket.dev/blog/tanstack-npm-packages-compromised-mini-shai-hulud-supply-chain-attack) confirme les 84 versions, des attestations Sigstore valides, et rattache l'attaque à la campagne Mini Shai-Hulud.

[^2]: [Elastic Security Labs](https://www.elastic.co/security-labs/threat-command/shai-hulud-chaindrop-npm-supply-chain) décrit le déclenchement par `preinstall`, la propagation par jetons volés et plus de 400 paquets compromis. [Aikido](https://www.aikido.dev/blog/keyv-and-friends-compromised-in-npm-supply-chain-attack) relève l'entrée `"preinstall": "node setup.mjs"`, au moins 444 paquets sur 1 381 versions le 5 août, et des versions publiées « with valid provenance signed by GitHub Actions ». [Endor Labs](https://www.endorlabs.com/learn/npm-malware-compromises-keyv-and-cacheable-with-500m-weekly-downloads-and-spreads-to-hundreds-of-packages) date le commit de 9 h 02 UTC et confirme la provenance SLSA valide.

[^3]: L'[alerte ouverte le 4 août 2026 sur le dépôt cacheable](https://github.com/jaredwray/cacheable/issues/1692) liste les versions compromises et ajoute : « Valid provenance on any subsequent release from these repos is not evidence of cleanliness ». Le mainteneur y indique ensuite que npm a retiré les neuf versions malveillantes, et qu'il a désactivé ses workflows et la publication de confiance.

[^4]: Le [billet de l'équipe Go sur la sécurité de la chaîne d'approvisionnement](https://go.dev/blog/supply-chain) détaille ce choix, en opposition explicite aux écosystèmes à hooks d'installation, et rappelle que la version de chaque dépendance est « fully determined by the go.mod file of the main module ».

[^5]: La [référence des modules Go](https://go.dev/ref/mod) décrit la base de sommes de contrôle, servie par `sum.golang.org` et adossée à Trillian, qui rend possibles les proxys non fiables puisqu'ils « can't serve the wrong code without it going unnoticed ». Elle précise aussi que, depuis Go 1.16, les commandes se comportent comme si `-mod=readonly` était activé.

[^6]: L'avis [GO-2025-3451 de la base de vulnérabilités de Go](https://pkg.go.dev/vuln/GO-2025-3451), publié le 5 février 2025, qualifie le module de « malicious typosquat ». [The Hacker News](https://thehackernews.com/2025/02/malicious-go-package-exploits-module.html) rapporte les travaux de Socket : version publiée en novembre 2021, mise en cache par le miroir, étiquette réécrite ensuite sur un code propre.

[^7]: La [note de version de pnpm 10.0.0](https://github.com/pnpm/pnpm/releases/tag/v10.0.0) l'annonce comme un changement majeur : « Lifecycle scripts of dependencies are not executed during installation by default! », avec une liste blanche, `onlyBuiltDependencies`, pour les rares paquets qui en ont besoin.

[^8]: Le [billet de sortie de govulncheck 1.0](https://go.dev/blog/govulncheck), du 13 juillet 2023, explique que l'outil « reduces noise by prioritizing vulnerabilities in functions that your code is actually calling ».



