Ils ont d’abord retiré les versions sûres
Le 20 août, quelqu’un a publié un arrayref empoisonné sur crates.io puis, seize secondes plus tard, a retiré les versions sûres derrière lui. L’avertissement de Cargo pointait alors vers la seule version moderne encore debout. La charge tenait dans un script de build : type-checker le projet suffisait à l’exécuter. Bromure Agentic Coding transforme toute l’opération en un problème de chronomètre qu’elle perd : les crates empoisonnées ont vécu 86 minutes, et la barrière d’âge par défaut est de deux jours.
L’attaquant n’a eu besoin de piéger personne pour faire installer son malware. Il a fait disparaître les versions saines, et laissé le conseil de mise à jour du registre faire le reste.
Le 20 août à 07:15:00 UTC, [email protected] est apparu sur crates.io sous le compte de
son véritable mainteneur. Vingt-quatre secondes plus tard, l’attaquant a commencé à
retirer les versions 0.3.5 à 0.3.9 : seize secondes de travail scripté pour vider
l’étagère derrière elle.
Le yank est le mécanisme de rappel de crates.io, le levier qu’un mainteneur actionne quand une version se révèle cassée. Cargo y répond exactement comme on le souhaiterait. Il vous prévient qu’une version présente dans votre lockfile a été retirée, et il refuse de choisir une version retirée lors d’une résolution neuve. Tous les conseils qui suivent un tel avertissement vous disent de mettre à jour.
L’attaquant a donc retiré les versions sûres, laissé une seule version moderne debout, et laissé le fait de faire ce qu’il faut vous y conduire.
Quatre-vingt-dix minutes, du début à la fin
L’avis du Rust Security Response WG et la chronologie reconstituée par StepSecurity décrivent une opération que l’attaquant préparait depuis deux jours.
Le 18 août à 01:25:58, il a enregistré sur crates.io un compte nommé dtolney, à une
lettre de David Tolnay, dont le nom figure sur la plupart des crates proc-macro de
l’écosystème. Une demi-heure plus tard, il publiait un [email protected] propre, un
typosquat de proc-macro2 qui ne faisait absolument rien. Cette version n’existait que
pour donner un historique au compte.
La vraie est arrivée cinq heures plus tard. [email protected] est monté à 07:11:15 le
20 août avec un build.rs malveillant. Quatre minutes après, [email protected] sortait du
compte de son propriétaire légitime avec un seul changement : une nouvelle ligne de
dépendance pointant vers proc-macro1 ^1.0.107. Le Security Response WG est prudent au
sujet du mainteneur, et il a raison de l’être. Il ne croit pas que « l’auteur d’arrayref
ait agi de façon malveillante », et note que « son ordinateur ou ses identifiants sont
probablement compromis ». [email protected] a suivi à 07:34:07, [email protected] à
07:37:49.
Puis tout s’est arrêté presque aussi vite que ça avait commencé. L’équipe de recherche de
Nextron Systems a trouvé la crate malveillante et l’a signalée. crates.io a retiré
arrayref après 86 minutes, internment après 90 et append-only-vec après 107, a
supprimé six crates appartenant à l’attaquant et a verrouillé le compte compromis. RustSec
n’a relevé aucune preuve que quiconque ait compilé contre une version empoisonnée.
Quatre-vingt-dix minutes, c’est une réponse rapide. Pour quiconque a lancé un build dans la fenêtre, elle est quand même arrivée après.
Rien dans la bibliothèque n’avait besoin d’être appelé
La charge vit dans un script de build, et les scripts de build s’exécutent à la
compilation avec vos privilèges. The Hacker News rapporte que la charge se déclenchait
lors d’un cargo build, d’un cargo check et d’un cargo test.
Celui du milieu est l’option prudente : ce que votre éditeur lance à l’enregistrement, ce
que vous tapez quand vous voulez l’avis du compilateur sans rien exécuter. Ici, il
suffisait.
Le script lui-même est court et sans finesse une fois que vous l’avez sous les yeux. Il
réassemble https://23.254.165[.]112:9089/ à partir de fragments base64, désactive la
vérification des certificats TLS, télécharge une charge adaptée à votre OS et à votre
architecture, et la lance détachée. La touche élégante est std::mem::forget(child), qui
abandonne le handle pour que le processus échappe au job object de Cargo. L’implant
continue de tourner et le compilateur se termine sur un build vert.
Ce qui descend ensuite est un infostealer.
L’analyse de Wiz
le décrit émettant en HTTPS POST vers /49890878, collectant nom d’hôte, nom
d’utilisateur, OS et applications installées, et interrogeant les bases SQLite de mots de
passe de Chrome, Brave et Edge. Il persiste via une clé Run du Registre sous Windows, un
LaunchAgent sous macOS et un service utilisateur systemd sous Linux, et si son C2
disparaît, il bascule sur un algorithme de génération de domaines produisant dix domaines
.com tous les cinq jours. Wiz relie l’infrastructure à des activités nord-coréennes : le
même endpoint /49890878 que la campagne npm Mastra, un émetteur de certificat partagé et
l’usage répété d’une même plage Hostwinds.
Les binaires livrés couvrent Linux x86_64, Windows et macOS, plus macOS aarch64. Ce dernier, c’est un portable Apple Silicon. Quelqu’un a réfléchi à qui compile du Rust.
La dépendance que vous n’avez jamais choisie
arrayref totalise 245 millions de téléchargements, et presque personne ne l’a mise dans
un Cargo.toml. StepSecurity a compté 406 versions de crates dépendantes, arrayref se
trouvant sous winit et donc sous egui et iced, sous blake3 et blake2b_simd, et
sous une bonne partie de l’outillage Ethereum et Solana. internment et append-only-vec
ajoutent 19 millions de téléchargements à eux deux.
Le conseil de relire vos dépendances avant de les ajouter n’atteint donc pas ce cas. Personne ne l’a ajoutée. Elle est arrivée quatre niveaux plus bas, sous une crate que vous avez bien ajoutée, dans un diff de lockfile qui défile. Lire le code de la bibliothèque n’aurait pas aidé non plus. Personne n’a touché aux sources des trois crates empoisonnées ; tout le changement tenait dans une ligne de dépendance nommant une crate vieille de deux jours, publiée par un compte dont le nom avait l’air correct.
Pourquoi un blog sur le codage agentique s’y intéresse
Regardez à nouveau le déclencheur. Un avertissement de retrait apparaît dans la sortie du
build. Quelque chose l’efface en lançant cargo update.
Ce quelque chose n’est souvent plus une personne. Effacer un avertissement fait partie du ménage qu’un agent accomplit en route vers ce que vous lui avez demandé : il voit une version retirée, il connaît le correctif, il applique le correctif. Il travaille pendant que vous dormez, en boucle, sur toute une file d’attente. Il rafraîchit des lockfiles parce qu’un build a échoué et ajoute des crates parce qu’une fonctionnalité en réclamait une.
Un agent qui travaille sans surveillance est l’acteur le plus susceptible de s’être trouvé dans une fenêtre de 86 minutes, et le moins susceptible de s’arrêter devant une dépendance transitive inconnue publiée par un nom qu’il reconnaît. Donnez-lui un compte de mainteneur compromis, un typosquat à une lettre de David Tolnay et un avertissement de registre qui recommande la mise à jour, et il ne reste rien dans la chaîne que le jugement puisse rattraper.
Vous pouvez survivre à cela, parce que le jugement est de toute façon un mauvais endroit où trancher.
Bromure en fait un problème de chronomètre
Chaque profil Bromure Agentic Coding est livré avec une barrière d’âge activée, réglée à deux jours. Le proxy MITM de l’hôte voit chaque récupération de paquet effectuée par la VM, sur npm, PyPI, Cargo, RubyGems, Maven, NuGet, les modules Go et Packagist, et les versions plus jeunes que le seuil ne reviennent pas.
Cargo a droit au traitement complet. Bromure reconnaît index.crates.io, crates.io et
static.crates.io, puis réécrit les métadonnées de la crate au passage : il retire de la
liste les versions trop fraîches et fixe max_version, max_stable_version et
newest_version à la version la plus récente qui survit au filtre. Le résolveur de Cargo
n’apprend jamais qu’une version plus récente existe.
Maintenant, mettez les chiffres côte à côte.
Rien dans cette opération n’a atteint deux jours d’âge. [email protected] avait quatre
minutes quand arrayref l’a nommée comme dépendance. Aucune des deux crates n’est visible
pour un résolveur derrière la barrière, donc cargo update ne trouve rien de nouveau.
Vous le verrez à l’écran. Avec 0.3.5 à 0.3.9 retirées et 0.3.10 filtrée, une résolution
neuve d’arrayref peut revenir incapable de choisir une version, ce qui est une erreur
plutôt qu’un succès silencieux. Un Cargo.lock existant épinglé sur 0.3.9 continue de
compiler, parce qu’un retrait ne bloque que les nouvelles résolutions. Dans les deux cas,
vous passez ce jeudi matin à lire un message de résolveur plutôt qu’à faire tourner tous
les identifiants que vous possédez.
Bromure vérifie une seconde fois, pour le cas où le résolveur avait déjà sa réponse. Il
confronte chaque récupération d’artefact, le fichier .crate lui-même, à la date de
publication enregistrée, et renvoie un 451 qui nomme l’âge : publié il y a 41 minutes,
la politique exige 2 jours minimum. Un cache de métadonnées chaud n’achète rien à
l’attaquant.
Le premier saut du dropper ne mène nulle part
Supposons que vous ayez arrêté l’horloge. Quelqu’un exempte un paquet, ou baisse le seuil
le temps d’un après-midi. Le script de build doit encore joindre 23.254.165.112 sur le
port 9089.
Chaque profil Bromure porte sa propre politique de sortie, écrite comme un court jeu de règles de style pf :
allow web crates.io
allow web static.crates.io
allow web index.crates.io
allow tcp api.anthropic.com:443
default deny
Votre Mac l’applique à deux niveaux qui partagent un seul jeu de règles : le commutateur
virtuel, qui filtre sur l’IP de destination et le nom d’hôte capté dans le DNS, tous
protocoles confondus, et le proxy, qui filtre sur le SNI TLS et la méthode HTTP. Une IP
nue sur le port 9089 ne correspond à aucune règle de ce fichier, elle tombe donc dans
default deny et la connexion meurt du côté hôte de la frontière. Le DGA de stage 2, dix
nouveaux domaines .com tous les cinq jours, rencontre le même mur, parce qu’un nom généré
reste un nom que personne n’a mis sur la liste. Chaque refus atterrit comme une ligne
egress.firewall dans Window → Security Timeline, avec l’hôte, l’IP, le port et le
verdict, écrite là où le code à l’intérieur de la VM ne peut pas l’atteindre.
Rien à voler là-dedans
Lisez la liste de remédiation qui est arrivée avec cet incident : faire tourner les clés SSH, les jetons cloud, les jetons d’API, les clés de signature. Purger le cache du registre. Vider les couches de cache CI. Reconstruire les répertoires vendor. Sur chaque machine qui a compilé pendant la fenêtre.
Un espace de travail Bromure répond à l’essentiel de cette liste par une pièce vide.
Les vrais identifiants n’entrent jamais dans la VM. L’ANTHROPIC_API_KEY de ce système
invité est un substitut brm_…. Le kubeconfig contient des certificats client jetables.
L’authentification au registre de conteneurs est une chaîne Basic dérivée. Le jeton GitHub
est un faux, et la clé SSH est une clé ed25519 générée par profil pour cet espace de
travail, pas celle de votre portable. Le proxy MITM de l’hôte substitue la vraie valeur sur
la requête sortante, et AWS va un cran plus loin : l’hôte resigne la requête SigV4 avec le
vrai matériel cryptographique, si bien que tout ce qui contourne le proxy reçoit un
InvalidSignatureException d’Amazon plutôt qu’un appel qui fonctionne.
Un infostealer qui ratisse cet invité à la recherche d’identifiants trouve des identifiants. Ils se parsent, ils ont l’air justes, et ils ne valent rien hors de la machine.
Le reste de sa liste de courses se trouve du mauvais côté de l’hyperviseur. Chrome, Brave
et Edge gardent leurs bases de mots de passe sur votre Mac, dans un profil utilisateur
auquel l’invité n’a aucun chemin. Le LaunchAgent macOS qu’il veut écrire, la raison pour
laquelle il a livré un binaire macOS aarch64, n’a pas de macOS où s’écrire. Il peut
installer un service utilisateur systemd dans le home de l’invité, et cette couche a un
bouton : Erase home… ramène le /home/ubuntu du profil à son état d’après-clonage, et
Reset to base… reclone le disque système de l’espace de travail depuis l’image de base
en lecture seule.
Et vous avez déjà l’inventaire
La dernière ligne de chaque guide de remédiation pour cet incident est un grep : chercher
dans chaque Cargo.lock de chaque machine six noms de crates et trois chaînes de version,
puis chercher les archives dans ~/.cargo/registry/cache. C’est une consigne très bien et
un après-midi misérable, et elle ne marche que sur les machines que vous avez encore.
Bromure enregistre la réponse au fil de l’eau. Chaque récupération de paquet à travers le
proxy écrit une ligne supply_chain.fetch dans Window → Security Timeline avec
l’écosystème, le paquet, la version, le résultat et la raison du résultat, côté hôte, aux
côtés de chaque substitution d’identifiant et de chaque verdict du pare-feu. Quand le
prochain avis nommera une version et une fenêtre de deux heures, la question « est-ce que
quelque chose ici a récupéré ça ? » sera une recherche plutôt qu’une expédition.
Bromure Enterprise Manager agrège le même flux sur toute une flotte, ce qui est la forme sous laquelle la question arrive : est-ce que l’une des quatre-vingt-dix machines qui faisaient tourner des agents cette nuit-là l’a compilé.
Ce qu’il faut garder
crates.io s’en est bien tiré. Quatre-vingt-dix minutes du signalement au retrait, six crates de l’attaquant supprimées, le compte compromis verrouillé, un avis clair, et aucune preuve que quiconque ait été touché. L’équipe Rust s’est aussi donné la peine de ne pas blâmer un mainteneur dont les identifiants avaient été volés, ce qui est le bon réflexe et pas toujours le plus répandu.
Quelqu’un réutilisera le mécanisme, cependant, parce qu’il est bon marché et qu’il est astucieux. Un retrait est un levier public, instantané et d’une seule ligne qui fait recommander une mise à jour par un registre, et chaque écosystème en possède un. Ce qui fait tourner vos builds verra cette recommandation et agira dessus, et la chose qui agit dessus manque désormais souvent du jeudi matin de contexte permettant de savoir quelle crate est laquelle.
Alors placez la décision quelque part qui ne demande pas de contexte. Deux jours de plein jour entre la publication d’une version et votre build ne sont pas un jugement sur un paquet, un mainteneur ou un nom de compte à une lettre près. C’est une horloge, et cette opération a eu quatre-vingt-dix minutes.
Sources : Rust Security Response WG, « Supply chain attack on arrayref » (20 août 2026) · Wiz, « Rust Supply Chain Attack on arrayref: Significant Overlap with DPRK Campaigns » (20 août 2026) · StepSecurity, « arrayref, internment, and append-only-vec Poisoned by the proc-macro1 Build-Time Dropper » (20 août 2026) · The Hacker News, « Rust Supply Chain Attack Puts Build-Time Malware in Crates with 245 Million Downloads » (20 août 2026) · BleepingComputer, « Hackers poison arrayref Rust crate to push infostealer malware » (20 août 2026) · Semgrep, « Rust crates arrayref & append-only-vec compromised » (20 août 2026)