L'installation était propre
npm a cessé d'exécuter par défaut les scripts d'installation des dépendances en juillet. Le 17 septembre, Checkmarx a publié une campagne qui ne discute pas cette décision : le chargeur se trouve dans `BTree.prototype.set`, la méthode pour laquelle la bibliothèque existe, et se déclenche la première fois que l'on y range une clé valant 100. Elle a tourné onze semaines sur deux millions de téléchargements hebdomadaires, et son canal de commande est un contrat intelligent sur Ethereum Sepolia. Un agent de codage est la victime idéale, parce qu'exécuter le code, c'est le travail. Bromure Agentic Coding décide de ce que ce code peut atteindre avant qu'il ne s'exécute.
Bloquer les scripts d'installation était la bonne décision, et l'écosystème npm l'a prise en juillet. Cette campagne a déplacé son exécution à la première utilisation du paquet : l'installation reste propre et c'est votre suite de tests qui appuie sur la détente.
Votre agent a besoin d'une table ordonnée. Il choisit un paquet d'arbre B sur
npm, l'ajoute à package.json et l'installe. Rien ne s'exécute. npm 12
n'exécute plus les scripts d'installation des dépendances : il n'y a donc pas de
postinstall, pas de preinstall, pas de hook de build, rien qu'une politique
sur les scripts de cycle de vie puisse refuser. L'agent écrit le code qui
utilise la nouvelle dépendance, écrit un test qui insère quelques centaines de
lignes, et l'exécute.
La centième insertion démarre un second processus Node.
Checkmarx a publié cette campagne le 17 septembre,
sur des recherches de Bruno Dias.
BleepingComputer l'a couverte le 20
et InfoWorld le 21.
Le paquet s'appelle indexed-btree, conçu pour être pris pour le vrai
sorted-btree, et quand npm l'a retiré le 3 septembre il approchait les deux
millions de téléchargements par semaine. Il était en ligne, dans sa forme
malveillante, depuis environ onze semaines.
La défense que tout le monde venait de finir de construire
En juin, la CLI npm a inversé son réglage par défaut sur les scripts
d'installation, et
npm 12 a livré ce changement en juillet :
les preinstall, install et postinstall d'une dépendance ne s'exécutent plus
à moins que le projet ne les autorise nommément, et les dépendances git ou par
URL distante ont cessé elles aussi de se résoudre par défaut. Les chiffres de
JFrog, cités dans cette couverture, placent ces trois vecteurs dans environ 53 %
des paquets npm malveillants observés l'année précédente. Les harnais d'agents
ont adopté la même posture : --ignore-scripts, ou un proxy qui retire les
hooks de l'archive avant même que le gestionnaire de paquets ne la voie. En
comptant onze semaines à rebours depuis la date de retrait, la première version
malveillante d'indexed-btree tombe au milieu de ce déploiement.
Les opérateurs ont donc cessé d'utiliser les scripts d'installation. Le chargeur
vit dans BTree.prototype.set, la méthode pour laquelle la bibliothèque existe.
Quand la clé qu'on lui passe vaut 100, le code lance un chargeur de première
étape obfusqué dans un processus Node détaché, depuis
extended/sharedLoad.min.js. Le résumé de Checkmarx est précis sur la raison
pour laquelle cela est difficile à voir venir : le chargeur se cache dans la
méthode même de la bibliothèque, ce qui constitue, selon leurs mots, « une
manière bien faite de passer sous le nez des outils standard d'analyse de
teinte ».
Regardez ce qui l'arme : une clé valant 100, dans une bibliothèque dont la raison d'être est de contenir des clés. Un classement l'atteint, un index paginé l'atteint, et un test qui insère des lignes en boucle l'atteint à la centième itération. Utilisez le paquet comme son README vous le dit, et vous l'armez.
Onze semaines, et un dépôt qui se lisait bien
Le reste du savoir-faire vise les vérifications que vous feriez vous-même.
Le canal de commande est un contrat intelligent sur Ethereum Sepolia. Le chargeur y lit sa prochaine adresse, génère une paire de clés X25519, prend la clé publique de l'opérateur dans le contrat, dérive une clé AES par ECDH, et déchiffre deux textes chiffrés stockés sur la chaîne pour assembler la seconde étape. Checkmarx est net sur le bénéfice : « Cette technique résiste mieux au retrait de domaine ou d'IP que les approches C2 traditionnelles, puisqu'elle utilise le contrat intelligent comme pointeur vers une nouvelle adresse dès que l'ancienne est démantelée. » Rien là-dedans ne vous donne un domaine à mettre en liste noire. Vous obtenez un point d'accès RPC public et une adresse de contrat, et les opérateurs peuvent réorienter le contrat sans toucher à une seule copie installée du paquet.
Le projet avait l'air vrai. Les opérateurs ont monté un dépôt GitHub avec un historique de commits plausible et un compte de développeur assorti, et le code de ce dépôt ne contient pas la partie malveillante. Elle n'est que dans l'archive npm. Lire le source sur GitHub, ce que tout le monde conseille, aurait confirmé que le paquet allait bien.
L'âge a joué pour eux aussi. Onze semaines font d'un paquet une dépendance
établie au moment où vous l'installez, rien à voir avec la publication détournée
retirée en quelques heures. Checkmarx a compté neuf paquets frères au même
comportement, btree-core à lui seul au-delà de 1,9 million de téléchargements
et les autres à quelques centaines de milliers chacun, et indique que les
opérateurs contrôlent un portefeuille détenant 109 ETH.
Puis, une fois la seconde étape en place, le chargeur efface ses propres fichiers et retire le déclencheur du prototype. Le paquet sur le disque redevient une bibliothèque d'arbre B comme les autres.
Exécuter le code, c'est le travail
Toutes les défenses ci-dessus supposent un moment où une personne décide d'exécuter quelque chose. Un agent supprime ce moment.
Un agent de codage installe une dépendance, écrit du code qui s'en sert, lance la suite de tests, lit l'échec, corrige et relance, des dizaines de fois par heure, pendant que vous êtes en réunion, parce que cette boucle est toute sa proposition de valeur. Vous ne lisez pas la centième insertion. L'agent a aussi plus de raisons que vous de produire le genre de données qui déclenchent ce piège : il écrit des fixtures en boucle, remplit des tables avec des identifiants séquentiels, et mesure les performances sur quelques milliers de lignes. Si la condition d'armement est « quelqu'un range la clé 100 », un agent y arrive plus tôt et plus souvent qu'une personne, dans un processus qui détient tout ce que détient la session.
Les défenses à l'installation couvrent le moment où un paquet arrive. Votre agent y passe une minute par jour, et tout le reste à exécuter ce qui est arrivé.
Ce dont le chargeur a besoin
Réduisez la campagne à l'os et il lui faut deux choses : un endroit où se connecter, et quelque chose qui vaille la peine d'être pris sur la machine où il s'est réveillé. Vous décidez des deux avant qu'il ne s'exécute, et aucune de ces décisions ne demande de reconnaître le paquet.
Sur un portable de développeur, les deux tournent à l'avantage de l'attaquant.
La reconnaissance qu'il envoie — nom d'hôte, architecture, CPU, mémoire,
uptime — décrit votre machine sur votre réseau. Le processus qu'il lance
s'exécute en tant que vous, avec votre shell, votre ~/.git-credentials, votre
GH_TOKEN, votre ~/.aws, votre agent SSH. Ses trois destinations sont
slack.com, api.telegram.org et un fournisseur RPC Ethereum public : deux
hôtes SaaS auxquels la moitié de votre outillage parle déjà, et un point d'accès
blockchain qui ressemble à de l'infrastructure. Rien dans cette liste ne se lit
comme malveillant dans un journal netflow.
Bromure Agentic Coding répond aux deux moitiés sur l'hôte, en dehors de la VM où tourne l'agent.
Les identifiants viennent d'abord, parce que ce sont eux la cible. Chaque secret
que vous configurez existe à l'intérieur de la VM de l'espace de travail sous
forme de marqueur préservant la structure (un jeton GitHub, c'est ghp_ suivi
de 36 caractères, une clé Anthropic commence par sk-ant-api03-brm-), dérivé de
la vraie valeur avec HKDF-SHA256 et un sel propre à l'installation, de sorte que
gh, git et claude l'acceptent et qu'il ne semble jamais tourner d'une
session à l'autre. Les vraies valeurs restent chiffrées sur votre Mac, et le
proxy de l'hôte les substitue sur le fil après que la requête a quitté la VM,
uniquement pour l'hôte pour lequel cet identifiant a été forgé. Le manuel énonce
l'invariant : aucun fichier, aucune variable d'environnement, aucun processus à
l'intérieur de la VM ne contient jamais une vraie clé d'API, un vrai jeton
OAuth, un vrai secret AWS ou une vraie clé privée SSH. Un voleur qui lit
l'environnement collecte des marqueurs.
Emporter un marqueur là où il n'a rien à faire est en soi l'alarme. Le proxy inspecte chaque requête sortante à la recherche de faux destinés à un hôte pour lequel ils n'ont pas été forgés. Quand il en trouve un, le proxy bloque la requête avec un 451, la destination ne reçoit rien, et Bromure met la VM en pause. Il rattache de force une session détachée, teinte le cadre en rouge, et lève une alerte nommant l'aperçu du jeton, l'identifiant, l'hôte auquel il appartenait et l'hôte vers lequel il partait. Vous pouvez arrêter l'espace de travail, ou choisir Save for Investigation, qui exporte le disque système, le répertoire personnel et chaque dossier partagé avant de tout stopper. Ce chargeur efface ses fichiers après la poignée de main : qui regarde ensuite trouve un disque bien rangé. La pause en pleine requête vous donne la machine avant qu'elle ne range.
Ensuite, la portée. Les garde-fous embarquent un pare-feu de sortie par espace de travail : un jeu de règles ordonné, de style pf, sur chaque connexion qu'ouvre la VM, par hôte, CIDR, protocole, port et, pour les règles web, par méthode HTTP individuelle, avec un réglage Unmatched traffic sur Allow ou Deny. Mettez-le sur Deny, listez les registres et les API dont le travail a besoin, et un chargeur qui tend la main vers une API de bot Telegram ou un fournisseur RPC Ethereum rencontre un refus sur le fil. Deux couches l'appliquent, pour que l'invité ne puisse contourner ni l'une ni l'autre : le commutateur virtuel fait correspondre les règles par IP de destination et par nom d'hôte capté dans le DNS, en TCP comme en UDP, et le proxy les fait correspondre à nouveau par nom de serveur TLS et par méthode. Le commutateur détourne les flux des ports 80 et 443 de la VM vers le proxy lui-même : il n'y a donc aucune variable de proxy qu'une charge utile puisse désactiver. Chaque verdict atterrit comme une ligne Firewall dans la Security Timeline, et les modifications de règles atteignent les sessions en cours sans redémarrage.
Là où le calendrier n'atteint pas
Le filtre d'ancienneté de Bromure, actif par défaut à deux jours, existe pour le schéma du détournement : un compte de mainteneur compromis, une version empoisonnée publiée, le tout repéré et retiré en quelques heures. Il a porté plusieurs de ces histoires. Onze semaines passent n'importe quel seuil que vous fixeriez, donc le travail revient ici à d'autres couches.
Le volet Supply Chain est une pile pour cette raison. La couche conçue pour les paquets malveillants dès l'origine, c'est le filtrage de paquets : le blocage des paquets compromis de socket.dev couvre les paquets signalés comme malware, les typosquats, les scripts d'installation douteux et le code obfusqué, et Depi remplace le registre npm par un miroir pré-vérifié. Les deux tournent côté hôte, leurs clés d'API restant sur votre Mac et n'étant jamais exportées dans la VM, et les deux se placent devant une installation plutôt que derrière. Un blocage revient sous forme de HTTP 451 dont npm imprime mot pour mot le corps en texte brut, de sorte que l'agent lit la raison et contourne au lieu de rester suspendu.
Aucun service de réputation n'attrape tout, et un typosquat resté propre onze semaines est le cas difficile pour chacun d'eux, ce qui est précisément là où le reste de la pile justifie sa place. Les marqueurs d'identifiants, le détecteur de compromission et le jeu de règles de sortie décident de ce qu'un paquet peut atteindre une fois installé, et aucun d'eux n'a à reconnaître le paquet. Ils couvrent le code que personne n'a encore classé, y compris le code que personne ne classera avant onze semaines de plus.
Supposez un déclenchement à la première utilisation
Bloquer les scripts de cycle de vie vaut la peine, et cette campagne le contourne. Prévoyez une charge utile qui se réveille quand votre suite de tests appelle la bibliothèque.
Le déclencheur est une donnée valide
Une clé valant 100 dans une bibliothèque clé-valeur. Aucune liste blanche d'entrées n'aide, et un agent génère cette donnée plus vite que vous.
Ne laissez rien à voler dans le processus
Un profil qui décrit une VM et des jetons sans valeur en amont transforment une exécution réussie en exécution perdue.
Décidez des destinations à l'avance
Slack, Telegram et un point d'accès RPC Ethereum public ne sont pas blocables par réputation. Une liste d'autorisation n'a pas à les reconnaître.
Bloquer les scripts d'installation a sorti du jeu environ la moitié des paquets
npm malveillants. C'est pourquoi les opérateurs ont dû construire quelque chose
d'aussi élaboré — un faux dépôt, un déclencheur déguisé en arithmétique et un
canal de commande sur une blockchain — pour placer du code qui, il y a un an,
demandait quatre lignes dans un package.json. La défense a fonctionné, et le
prix de l'attaque a monté.
L'hypothèse en dessous, elle, n'a pas bougé. Vous considérez toujours une dépendance comme sûre une fois installée, et son exécution comme un acte neutre. Pour un agent de codage, l'exécuter est le travail, des milliers de fois par jour, sans personne dans la pièce. Décidez de ce que ce processus peut détenir et de l'endroit où il peut se connecter avant qu'il ne s'exécute, et la centième insertion de votre agent finira en ligne Firewall bloquée dans la Security Timeline.