Primero retiraron las versiones seguras
El 20 de agosto alguien publicó un arrayref envenenado en crates.io y, dieciséis segundos después, retiró las versiones seguras que quedaban detrás. El propio aviso de Cargo pasó entonces a señalar la única versión moderna que seguía en pie. La carga estaba en un script de compilación, así que con hacer type-check del proyecto bastaba para ejecutarla. Bromure Agentic Coding convierte toda la operación en un problema de cronómetro que ella pierde: los crates envenenados vivieron 86 minutos, y la barrera de edad por defecto es de dos días.
El atacante no tuvo que engañar a nadie para que instalara malware. Hizo desaparecer las versiones limpias y dejó que el propio consejo de actualización del registro hiciera el resto.
A las 07:15:00 UTC del 20 de agosto, [email protected] apareció en crates.io bajo la cuenta
de su verdadero mantenedor. Veinticuatro segundos más tarde el atacante empezó a retirar
las versiones 0.3.5 a 0.3.9: dieciséis segundos de trabajo automatizado para vaciar el
estante que quedaba detrás.
El yank es el mecanismo de retirada de crates.io, la palanca que un mantenedor acciona cuando una versión sale rota. Cargo responde a eso como uno querría. Te avisa de que una versión de tu lockfile ha sido retirada y se niega a elegir una versión retirada en una resolución nueva. Todo consejo que sigue a un aviso así te dice que actualices.
Así que el atacante retiró las versiones seguras, dejó en pie una sola versión moderna y dejó que hacer lo correcto te llevara directo a ella.
Noventa minutos, de principio a fin
El aviso del Rust Security Response WG y la cronología reconstruida por StepSecurity describen una operación que el atacante llevaba dos días preparando.
El 18 de agosto a las 01:25:58 registró en crates.io una cuenta llamada dtolney, a una
letra de David Tolnay, cuyo nombre figura en la mayoría de los crates proc-macro del
ecosistema. Media hora después publicó un [email protected] limpio, un typosquat de
proc-macro2 que no hacía absolutamente nada. Esa versión existía para darle historial a
la cuenta.
La de verdad llegó cinco horas más tarde. [email protected] subió a las 07:11:15 del 20
de agosto con un build.rs malicioso. Cuatro minutos después, [email protected] salió de la
cuenta de su dueño legítimo con un solo cambio: una nueva línea de dependencia que apuntaba
a proc-macro1 ^1.0.107. El Security Response WG es cuidadoso con el mantenedor aquí, y
hace bien. Dicen que “no creen que el autor de arrayref esté actuando de forma maliciosa”
y señalan que “su ordenador o sus credenciales están probablemente comprometidos”.
[email protected] siguió a las 07:34:07 y [email protected] a las 07:37:49.
Después terminó casi tan rápido como había empezado. El equipo de investigación de Nextron
Systems encontró el crate malicioso y lo reportó. crates.io retiró arrayref a los 86
minutos, internment a los 90 y append-only-vec a los 107, borró seis crates del
atacante y bloqueó la cuenta comprometida. RustSec no registró ninguna evidencia de que
nadie compilara contra una versión envenenada.
Noventa minutos es una respuesta rápida. Para quien tuviera una compilación corriendo dentro de la ventana, aun así llegó después.
Nada de la biblioteca tenía que llamarse
La carga vive en un script de compilación, y los scripts de compilación se ejecutan en
tiempo de compilación con tus privilegios. The Hacker News informa de que la carga se
disparaba durante
cargo build, cargo check y cargo test.
El del medio es la opción prudente: lo que tu editor ejecuta al guardar, lo que tecleas
cuando quieres la opinión del compilador sin ejecutar nada. Aquí bastó.
El script en sí es corto y nada sutil una vez lo tienes delante. Reensambla
https://23.254.165[.]112:9089/ a partir de fragmentos base64, desactiva la verificación
de certificados TLS, descarga una carga acorde a tu sistema operativo y arquitectura, y la
ejecuta desacoplada. El detalle elegante es std::mem::forget(child), que suelta el
handle para que el proceso escape del job object de Cargo. El implante sigue corriendo y el
compilador termina con una compilación en verde.
Lo que baja después es un infostealer.
El análisis de Wiz
lo describe emitiendo balizas por HTTPS POST hacia /49890878, recogiendo nombre de host,
nombre de usuario, sistema operativo y aplicaciones instaladas, y consultando las bases
SQLite de contraseñas de Chrome, Brave y Edge. Persiste como clave Run del Registro en
Windows, como LaunchAgent en macOS y como servicio de usuario systemd en Linux, y si su C2
se apaga recurre a un algoritmo de generación de dominios que produce diez dominios .com
cada cinco días. Wiz vincula la infraestructura con actividad de la RPDC: el mismo endpoint
/49890878 que la campaña npm de Mastra, un emisor de certificados compartido y el uso
repetido de un rango de Hostwinds.
Los binarios que envió cubren Linux x86_64, Windows y macOS, más macOS aarch64. Ese último es un portátil con Apple Silicon. Alguien pensó en quién compila Rust.
La dependencia que nunca elegiste
arrayref tiene 245 millones de descargas acumuladas, y casi nadie la puso en un
Cargo.toml. StepSecurity contó 406 versiones de crates dependientes, con arrayref
sentado bajo winit y por tanto bajo egui e iced, bajo blake3 y blake2b_simd, y
bajo buena parte del utillaje de Ethereum y Solana. internment y append-only-vec suman
otros 19 millones de descargas entre las dos.
Así que el consejo de revisar tus dependencias antes de añadirlas no llega hasta aquí. Nadie la añadió. Llegó cuatro niveles más abajo, bajo un crate que sí añadiste, en un diff de lockfile que pasa de largo. Leer el código de la biblioteca tampoco habría servido. Nadie tocó el código fuente de los tres crates envenenados; todo el cambio era una línea de dependencia que nombraba un crate de dos días de antigüedad, publicado por una cuenta cuyo nombre parecía correcto.
Por qué le importa a un blog sobre programación agéntica
Mira otra vez el disparador. Un aviso de retirada aparece en la salida de la compilación.
Algo lo limpia ejecutando cargo update.
Ese algo muchas veces ya no es una persona. Limpiar un aviso es la tarea doméstica que un agente hace de camino a lo que le pediste: ve una versión retirada, conoce el arreglo, aplica el arreglo. Trabaja mientras duermes, en bucle, sobre una cola entera de tareas. Refresca lockfiles porque una compilación falló y añade crates porque una funcionalidad necesitaba uno.
Un agente trabajando sin supervisión es el actor con más probabilidades de haber estado dentro de una ventana de 86 minutos, y el que menos probabilidades tiene de detenerse ante una dependencia transitiva desconocida publicada por un nombre que reconoce. Dale una cuenta de mantenedor comprometida, un typosquat a una letra de David Tolnay y un aviso del registro que recomienda la actualización, y no queda nada en la cadena que el criterio pueda atrapar.
Puedes sobrevivir a eso, porque el criterio es de todos modos un mal sitio donde decidir esto.
Bromure lo convierte en un problema de cronómetro
Todo perfil de Bromure Agentic Coding viene con una barrera de edad activada, fijada en dos días. El proxy MITM del host ve cada descarga de paquete que hace la VM, en npm, PyPI, Cargo, RubyGems, Maven, NuGet, módulos de Go y Packagist, y las versiones más jóvenes que el corte no vuelven.
Cargo recibe el tratamiento completo. Bromure reconoce index.crates.io, crates.io y
static.crates.io, y entonces reescribe los metadatos del crate al vuelo: quita de la
lista de versiones las demasiado frescas y fija max_version, max_stable_version y
newest_version en la versión más reciente que sobrevive al filtro. El resolutor de Cargo
nunca llega a enterarse de que existe una versión más nueva.
Ahora pon los números uno al lado del otro.
Nada en esta operación llegó a los dos días de antigüedad. [email protected] tenía
cuatro minutos cuando arrayref lo nombró como dependencia. Ninguno de los dos crates es
visible para un resolutor que esté detrás de la barrera, así que cargo update no encuentra
nada nuevo.
Lo verás en tu pantalla. Con 0.3.5 a 0.3.9 retiradas y 0.3.10 filtrada, una resolución
nueva de arrayref puede volver sin poder elegir una versión, lo cual es un error en
lugar de un éxito silencioso. Un Cargo.lock existente fijado en 0.3.9 sigue compilando,
porque una retirada solo bloquea la resolución nueva. En cualquier caso pasas ese jueves por
la mañana leyendo un mensaje del resolutor en vez de rotando todas las credenciales que
tienes.
Bromure comprueba una segunda vez, por si el resolutor ya tenía su respuesta. Contrasta cada
descarga de artefacto, el propio fichero .crate, con la hora de publicación registrada, y
devuelve un 451 que nombra la edad: publicado hace 41 minutos, la política exige 2 días
como mínimo. Una caché de metadatos caliente no le compra nada al atacante.
El primer salto del dropper no lleva a ninguna parte
Supón que hubieras apagado el reloj. Alguien exime un paquete, o baja el corte durante una
tarde. El script de compilación todavía tiene que alcanzar 23.254.165.112 en el puerto
9089.
Cada perfil de Bromure lleva su propia política de salida, escrita como un pequeño conjunto de reglas al estilo pf:
allow web crates.io
allow web static.crates.io
allow web index.crates.io
allow tcp api.anthropic.com:443
default deny
Tu Mac la aplica en dos capas que comparten un único conjunto de reglas: el conmutador
virtual, que casa por IP de destino y por nombre de host espiado en el DNS a través de todos
los protocolos, y el proxy, que casa por SNI de TLS y método HTTP. Una IP pelada en el
puerto 9089 no casa con ninguna regla de ese fichero, así que cae en default deny y la
conexión muere del lado del host de la frontera. El DGA de la fase 2, diez dominios .com
nuevos cada cinco días, se topa con el mismo muro, porque un nombre generado sigue siendo un
nombre que nadie puso en la lista. Cada denegación aterriza como una fila egress.firewall
en Window → Security Timeline, con host, IP, puerto y veredicto, escrita donde el código
de dentro de la VM no puede llegar.
Ahí dentro no hay nada que robar
Lee la lista de recuperación que vino con este incidente: rotar claves SSH, tokens de nube, tokens de API, claves de firma. Purgar la caché del registro. Limpiar las capas de caché de CI. Reconstruir los directorios vendor. En cada máquina que compiló durante la ventana.
Un espacio de trabajo Bromure responde a casi toda esa lista con una habitación vacía.
Las credenciales de verdad nunca entran en la VM. La ANTHROPIC_API_KEY de ese invitado es
un marcador brm_…. El kubeconfig guarda certificados de cliente desechables. La
autenticación del registro de contenedores es una cadena Basic derivada. El token de GitHub
es falso, y la clave SSH es una clave ed25519 generada por perfil para ese espacio de
trabajo, no la de tu portátil. El proxy MITM del host sustituye el valor real en la petición
saliente, y con AWS va un paso más allá: el host vuelve a firmar la petición SigV4 con el
material real, de modo que cualquier cosa que rodee el proxy recibe un
InvalidSignatureException de Amazon en lugar de una llamada que funcione.
Un infostealer que barre ese invitado buscando credenciales encuentra credenciales. Se parsean, parecen correctas y no valen nada fuera de la máquina.
El resto de su lista de la compra está al otro lado del hipervisor. Chrome, Brave y Edge
guardan sus bases de contraseñas en tu Mac, en un perfil de usuario al que el invitado no
tiene camino. El LaunchAgent de macOS que quiere escribir, la razón por la que envió un
binario macOS aarch64, no tiene ningún macOS donde escribirse. Puede instalar un servicio de
usuario systemd en el propio home del invitado, y esa capa tiene un botón: Erase home…
devuelve el /home/ubuntu del perfil a su estado justo después del clonado, y
Reset to base… vuelve a clonar el disco de sistema del espacio de trabajo desde la
imagen base de solo lectura.
Y el inventario ya lo tienes
La última línea de toda guía de remediación para este incidente es un grep: buscar en cada
Cargo.lock de cada máquina seis nombres de crate y tres cadenas de versión, y después
buscar los tarballs en ~/.cargo/registry/cache. Es una instrucción estupenda y una tarde
miserable, y solo funciona en las máquinas que todavía conservas.
Bromure registra la respuesta según ocurre. Cada descarga de paquete a través del proxy
escribe una fila supply_chain.fetch en Window → Security Timeline con el ecosistema,
el paquete, la versión, el resultado y la razón detrás del resultado, del lado del host,
junto a cada sustitución de credencial y cada veredicto del cortafuegos. Cuando el próximo
aviso nombre una versión y una ventana de dos horas, la pregunta “¿algo de aquí descargó
eso?” será una búsqueda en vez de una expedición.
Bromure Enterprise Manager agrega el mismo flujo a lo largo de una flota, que es la forma en la que llega la pregunta: ¿alguna de las noventa máquinas que corrían agentes durante la noche lo compiló?
La parte que vale la pena guardar
crates.io lo hizo bien aquí. Noventa minutos desde el reporte hasta la retirada, seis crates del atacante eliminados, la cuenta comprometida bloqueada, un aviso claro y ninguna evidencia de que nadie fuera golpeado. El equipo de Rust además se esforzó por no culpar a un mantenedor al que le robaron las credenciales, que es el instinto correcto y no siempre el habitual.
Alguien reutilizará el mecanismo, sin embargo, porque es barato y es ingenioso. Una retirada es una palanca pública, instantánea y de una sola línea que hace que un registro recomiende una actualización, y todos los ecosistemas tienen una. Lo que sea que ejecute tus compilaciones verá esa recomendación y actuará en consecuencia, y a lo que actúa hoy en consecuencia le falta a menudo un jueves por la mañana de contexto sobre qué crate es cuál.
Así que pon la decisión en algún sitio que no exija contexto. Dos días de luz entre la publicación de una versión y tu compilación no son un juicio sobre un paquete, un mantenedor o un nombre de cuenta que se queda a una letra. Son un reloj, y esta operación tuvo noventa minutos.
Fuentes: Rust Security Response WG, “Supply chain attack on arrayref” (20 de agosto de 2026) · Wiz, “Rust Supply Chain Attack on arrayref: Significant Overlap with DPRK Campaigns” (20 de agosto de 2026) · StepSecurity, “arrayref, internment, and append-only-vec Poisoned by the proc-macro1 Build-Time Dropper” (20 de agosto de 2026) · The Hacker News, “Rust Supply Chain Attack Puts Build-Time Malware in Crates with 245 Million Downloads” (20 de agosto de 2026) · BleepingComputer, “Hackers poison arrayref Rust crate to push infostealer malware” (20 de agosto de 2026) · Semgrep, “Rust crates arrayref & append-only-vec compromised” (20 de agosto de 2026)