# Ce qu'un paquet malveillant lit dans tes variables d'environnement

> Un script d'installation piégé lit son propre environnement, les fichiers de ton dossier personnel, et parfois l'environnement des autres processus. J'ai mesuré ce qu'il voit avec un petit programme Go, et j'ai trié les parades entre celles qui marchent et celles qui rassurent.


*Ou : Comment une seule commande d'installation se retrouve avec les clés de la maison*

Il y a un geste qu'on fait des dizaines de fois par jour sans y penser : installer une dépendance. `npm install`, `pip install`, un `go get`. On tape la commande, on regarde ailleurs pendant que ça télécharge, et on revient quand c'est fini. En 2025, deux vers ont transformé exactement ce moment d'inattention en moteur de propagation. Leur butin n'était pas sophistiqué. C'étaient tes variables d'environnement et quelques fichiers de ton dossier personnel, lus par du code qui tournait avec tes droits, parce que tu venais de l'inviter à tourner.

La bonne nouvelle, c'est que ce que ce code peut voir est parfaitement délimité, et mesurable. Prenons un café. On va regarder ce qu'un script d'installation voit vraiment, puis trier les parades entre celles qui ferment la porte et celles qui se contentent de la rendre jolie.

## Secrets exposés : ce qu'un script d'installation lit, et comment réduire la casse
{.subtitle}

### Deux vers, un même réflexe

Le 15 septembre 2025, un ver baptisé Shai-Hulud se répand dans npm. Il s'installe par un script `postinstall` qui lance `node bundle.js`, télécharge l'outil légitime TruffleHog, et le pointe sur le disque avec `trufflehog filesystem . --json --results=verified` pour y trouver des secrets vérifiés[^1]. Il cherche les jetons GitHub (`ghp_`, `gho_`), les jetons npm et les identifiants AWS et GCP, sur le disque et jusque dans les points d'accès de métadonnées du cloud. Puis il publie ses trouvailles dans un dépôt public nommé « Shai-Hulud » et republie les paquets de sa victime, infectés à leur tour. Plus de cinq cents paquets touchés[^1]. Sa deuxième vague, en novembre 2025, passe au script `preinstall` et range les secrets dans des fichiers dont l'un, `environment.json`, porte bien son nom[^2].

Trois semaines plus tôt, fin août 2025, l'attaque dite s1ngularity avait visé les paquets Nx. Même porte d'entrée, un script `postinstall` (un fichier `telemetry.js`), mais une idée en plus : au lieu de chercher lui-même, le code lançait les assistants d'IA en ligne de commande déjà installés sur la machine, Claude, Gemini et Q, avec des options qui désactivent les garde-fous, `--dangerously-skip-permissions`, `--yolo` et `--trust-all-tools`[^3]. Il leur demandait de dresser l'inventaire des fichiers sensibles. Bilan selon Wiz : plus de mille jetons GitHub valides, des dizaines d'identifiants cloud, et environ vingt mille fichiers exfiltrés[^3]. Le code ciblait les portefeuilles de cryptomonnaie, les `.env`, les clés SSH, le jeton de `gh` et le contenu de `.npmrc`.

Deux équipes différentes, deux outils différents, et pourtant le même geste : fouiller l'environnement et le dossier personnel, au moment de l'installation.

### Ce qu'un processus voit, mesuré

Pourquoi ce moment précis ? Parce qu'un script d'installation n'est pas un invité à qui on ouvre une pièce. Il hérite de ton identité. Tout ce que ton utilisateur peut lire, il peut le lire. Pour montrer l'étendue exacte, j'ai écrit un petit programme Go qui joue le rôle du script, et je l'ai lancé dans un faux dossier personnel rempli de faux secrets, sur Ubuntu 24.04 et Go 1.27.3. Voici le cœur de ce qu'il inspecte :

```go
// 1. Son propre environnement, hérité de qui l'a lancé
for _, kv := range os.Environ() {
	nom, valeur, _ := strings.Cut(kv, "=")
	if suspect.MatchString(nom) { // (?i)(token|secret|key|password|auth)
		fmt.Printf("environnement\t%s\t%d caractères\n", nom, len(valeur))
	}
}

// 2. Les fichiers connus du dossier personnel
home, _ := os.UserHomeDir()
for _, f := range []string{".env", ".npmrc", ".aws/credentials",
	".config/gh/hosts.yml", ".ssh/id_ed25519"} {
	if info, err := os.Stat(filepath.Join(home, f)); err == nil {
		fmt.Printf("fichier\t~/%s\t%d octets\n", f, info.Size())
	}
}

// 3. L'environnement d'un autre processus du même utilisateur
brut, err := os.ReadFile("/proc/" + voisin + "/environ")
```

À l'exécution, dans son bac isolé, il affiche :

```
environnement    NPM_TOKEN              36 caractères
environnement    OPENAI_API_KEY         45 caractères
fichier          ~/.env                 92 octets
fichier          ~/.npmrc               70 octets
fichier          ~/.aws/credentials    108 octets
fichier          ~/.config/gh/hosts.yml 83 octets
fichier          ~/.ssh/id_ed25519      75 octets
processus 387103 GITHUB_TOKEN           38 caractères
```

Les trois niveaux sont là. Son propre environnement d'abord : tout ce que le shell lui a transmis, `NPM_TOKEN` et `OPENAI_API_KEY` compris. Les fichiers ensuite, exactement ceux que les deux vers cherchaient. Et le troisième niveau mérite qu'on s'arrête : `/proc/387103/environ`, l'environnement d'un *autre* processus, lu sans aucun privilège spécial. Sous Linux, l'accès à ce fichier passe par la vérification « PTRACE_MODE_READ_FSCREDS »[^4]. En clair, un processus peut lire l'environnement d'un autre dès lors qu'ils appartiennent au même utilisateur. Le module de sécurité Yama, actif par défaut sur Ubuntu avec `ptrace_scope` à 1, restreint l'attachement d'un débogueur, pas cette simple lecture entre processus du même propriétaire. La variable que tu as exportée dans un terminal reste lisible par n'importe quel programme lancé depuis un autre terminal, tant qu'elle vit.

C'est le point que l'intuition rate. On croit qu'une variable d'environnement est privée à son shell. Elle est privée à ton *utilisateur*, ce qui n'est pas du tout la même chose quand une de tes dépendances se met à exécuter du code hostile.

### Les parades, et ce qu'elles ne font pas

Première règle, la plus rentable : ne rien exporter dans tout le shell. Un secret passé en préfixe d'une seule commande (`STRIPE_KEY=... ./ma-commande`) ne vit que le temps de cette commande et n'apparaît pas dans l'environnement des processus voisins. Sa limite est nette, et il faut la dire : pendant que la commande tourne, son propre code, et tout ce qu'elle charge, lit ce secret sans effort. On réduit la fenêtre, on ne la ferme pas.

Deuxième parade, sortir les secrets des fichiers en clair pour un trousseau du système ou un gestionnaire dédié. C'est mieux qu'un `.env` qui traîne, mais la protection a elle aussi un bord. Sur un poste Linux de bureau, le trousseau de session est déverrouillé dès l'ouverture de session, et n'importe quelle application lancée par le même utilisateur peut alors lire les secrets d'un trousseau déverrouillé. C'est l'objet de la CVE-2018-19358, que l'éditeur conteste au nom de son modèle de sécurité[^5]. Un gestionnaire de secrets protège surtout contre le secret qui dort en clair, moins contre le code qui tourne sous ton nom.

Côté installation, le terrain bouge dans le bon sens. pnpm a ouvert la voie le 7 janvier 2025 en cessant d'exécuter par défaut les scripts d'installation des dépendances[^6]. npm a suivi avec sa version 12, sortie le 8 juillet 2026 : les scripts de cycle de vie des dépendances sont désormais bloqués par défaut, sauf autorisation explicite dans le projet[^7]. Avant cette version, il fallait y penser soi-même avec `--ignore-scripts`. Et les modules Go, eux, n'ont jamais eu de script d'installation : `go get` télécharge, il n'exécute rien[^8]. Attention au mot « exécute », cela dit : `go test ./...` lance bien les fonctions `init()` de toutes tes dépendances, et une dépendance piégée y trouve l'environnement de ta machine d'intégration continue. Go déplace le moment du risque, il ne le supprime pas, comme [le détaille l'article sur le ver CHAINDROP et les modules Go](/blog/chaindrop-npm-modules-go-supply-chain/).

Reste la parade de fond : séparer. Un utilisateur dédié aux installations ne voit ni ton `.ssh` ni ton `.aws`. Mais méfie-toi d'une fausse amie : un conteneur de développement du type Distrobox ou Toolbx n'est pas un bac à sable. Les deux partagent ton dossier personnel avec l'hôte, et le disent clairement, « Isolation and sandboxing are not the main aims » pour l'un[^9], « makes no promise about security » pour l'autre[^10]. Pour une vraie barrière, il faut une machine virtuelle, un conteneur Podman en mode rootless, ou une application confinée par Flatpak. C'est exactement le genre de vérification qui devrait figurer sur [ta liste de contrôle quand tu relis le code d'un agent](/blog/relire-pr-agent-code-liste-controle/).

**DevOps Dave :** Mes secrets sont dans des variables d'environnement, pas dans un fichier. C'est plus propre.

**Security Sarah :** Plus propre pour qui ? Le script qui s'installe les lit en une ligne.

**DevOps Dave :** Mais je les ai exportés dans mon shell, pas dans le sien.

**Security Sarah :** Vous avez le même nom d'utilisateur. Pour le noyau, c'est la même personne.

Le vrai changement de modèle est là. Pendant des années, la frontière de confiance passait entre les utilisateurs d'une machine. Les vers de 2025 ont montré qu'elle passe désormais entre toi et chaque dépendance que tu installes. Tes secrets ne sont pas protégés parce qu'ils sont à toi. Ils sont exposés à tout ce qui tourne sous ton nom, et tu viens d'en inviter un peu plus.

[^1]: Le [billet de Sysdig du 16 septembre 2025](https://www.sysdig.com/blog/shai-hulud-the-novel-self-replicating-worm-infecting-hundreds-of-npm-packages) décrit le déclenchement par `postinstall` (`node bundle.js`), le téléchargement de TruffleHog et la commande `trufflehog filesystem . --json --results=verified`, la recherche des jetons GitHub `ghp_`/`gho_`, npm, AWS et GCP sur le disque et dans les points de métadonnées (IMDS), ainsi que la propagation par republication. L'[alerte de la CISA du 23 septembre 2025](https://www.cisa.gov/news-events/alerts/2025/09/23/widespread-supply-chain-compromise-impacting-npm-ecosystem) confirme plus de cinq cents paquets compromis et l'exfiltration vers un dépôt public « Shai-Hulud ».

[^2]: L'[analyse de Wiz sur la vague de novembre 2025](https://www.wiz.io/blog/shai-hulud-2-0-ongoing-supply-chain-attack) indique que cette variante « executes only during the preinstall phase », qu'elle récolte les identifiants depuis les fichiers de configuration, les variables d'environnement et les métadonnées cloud, et liste les fichiers créés, dont `environment.json`. Les versions piégées ont été publiées entre le 21 et le 23 novembre 2025.

[^3]: L'[analyse de Wiz sur s1ngularity](https://www.wiz.io/blog/s1ngularity-supply-chain-attack) décrit le fichier `telemetry.js` déclenché à l'installation, l'appel des outils d'IA en ligne de commande (Claude, Gemini, Q) avec les options `--dangerously-skip-permissions`, `--yolo` et `--trust-all-tools`, les cibles (portefeuilles, `.env`, clés SSH, jeton `gh`, contenu de `.npmrc`), et le bilan de la première phase : plus de mille jetons GitHub valides et environ vingt mille fichiers. L'[avis de sécurité GitHub GHSA-cxm3-wv7p-598c](https://github.com/nrwl/nx/security/advisories/GHSA-cxm3-wv7p-598c) donne les versions de Nx touchées et leurs horaires de publication les 26 et 27 août 2025.

[^4]: La page de manuel [proc_pid_environ(5)](https://man7.org/linux/man-pages/man5/proc_pid_environ.5.html) précise que « Permission to access this file is governed by a ptrace access mode PTRACE_MODE_READ_FSCREDS check ». La page [ptrace(2)](https://man7.org/linux/man-pages/man2/ptrace.2.html) détaille cet algorithme (accès autorisé quand les UID et GID réels, effectifs et sauvegardés de la cible correspondent à ceux de l'appelant) et le rôle du module Yama via `/proc/sys/kernel/yama/ptrace_scope`, qui restreint l'attachement par `PTRACE_MODE_ATTACH`, pas cette lecture. Mesure faite sur un noyau 6.8 avec `ptrace_scope` à 1.

[^5]: La fiche [CVE-2018-19358](https://nvd.nist.gov/vuln/detail/cve-2018-19358) : « GNOME Keyring through 3.28.2 allows local users to retrieve login credentials via a Secret Service API call and the D-Bus interface if the keyring is unlocked ». La note ajoute que l'éditeur conteste, au motif que son modèle de sécurité interdit aux applications non fiables d'accéder au bus de session de l'utilisateur.

[^6]: La [note de version de pnpm 10.0.0](https://github.com/pnpm/pnpm/releases/tag/v10.0.0), du 7 janvier 2025, 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 concernés.

[^7]: Le [changelog GitHub sur npm v12](https://github.blog/changelog/2026-06-09-upcoming-breaking-changes-for-npm-v12/) indique que « `allowScripts` defaults to off » et que « `npm install` will no longer execute `preinstall`, `install`, or `postinstall` scripts from dependencies » sans autorisation explicite ; les avertissements existent depuis npm 11.16.0. La [note de version de npm 12.0.0](https://github.com/npm/cli/releases/tag/v12.0.0), du 8 juillet 2026, confirme : « Dependency lifecycle scripts are now blocked by default unless allowed by the root package's `allowScripts` policy ». La [documentation de `ignore-scripts`](https://docs.npmjs.com/cli/v11/using-npm/config/#ignore-scripts) rappelle que l'option, à `false` par défaut, n'empêche pas `npm run`, `npm test` ou `npm start` de lancer leur propre script.

[^8]: Le [billet de l'équipe Go sur la sécurité de la chaîne d'approvisionnement](https://go.dev/blog/supply-chain) oppose explicitement le modèle des modules Go aux écosystèmes à hooks d'installation : la version de chaque dépendance est « fully determined by the go.mod file of the main module », et rien ne s'exécute au téléchargement.

[^9]: La [documentation de Distrobox](https://github.com/89luca89/distrobox/blob/main/docs/README.md), section « Security implications » : « Isolation and sandboxing are **not** the main aims of the project ». Le conteneur a plein accès au dossier personnel de l'hôte ; pour une isolation réelle, le projet renvoie vers Podman en mode rootless ou des outils dédiés.

[^10]: Le [README de Toolbx](https://github.com/containers/toolbox/blob/main/README.md) : les environnements ont un accès transparent au dossier personnel de l'utilisateur, et « Toolbx makes no promise about security beyond what's already available in the usual command line environment on the host ».



