DevSecOps

El ataque a arrayref: cómo 86 minutos en crates.io comprometieron 245 millones de descargas de Rust

El veinte de agosto de 2026, a las 07:15 UTC, alguien publicó una versión nueva del crate arrayref en crates.io. Setenta y seis minutos después, crates.io la borró. En esa ventana, una build script maliciosa se ejecutó en cada máquina que compiló un proyecto que resolvía la versión envenenada — y arrayref es dependencia transitiva de 403 crates distintos del ecosistema, con 245.385.500 descargas históricas y más de 53 millones en los últimos 90 días. El incidente no es un CVE abstracto: es un caso real de cómo el tiempo de compilación se convierte en ventana de ataque.

Lo que pasó, en orden cronológico

El Rust Security Response Team recibió el reporte inicial de Nextron Systems GmbH a las 07:15 UTC del 20 de agosto. En la hora siguiente, se publicaron tres versiones maliciosas desde una misma cuenta de maintainer:

- `arrayref@0.3.10`, publicada a las 07:15:00 UTC, borrada a las 08:41:40 UTC — 86 minutos en línea. - `internment@0.8.7`, publicada a las 07:34:07 UTC, borrada a las 09:04:11 UTC — 90 minutos. - `append-only-vec@0.1.9`, publicada a las 07:37:49 UTC, borrada a las 09:25:24 UTC — 107 minutos.

Cada release llevaba una sola línea añadida al manifiesto: una dependencia directa sobre `proc-macro1`. El nombre es un typosquat deliberado del ubicuo `proc-macro2`, presente en casi todo el ecosistema Rust que usa macros procedurales. La diferencia entre ambos es un dígito; el parecido, suficiente para que un revisor distraído no lo detecte.

El código fuente de `proc-macro1` es una copia legítima de `proc-macro2`, así que las builds que resolvían la dependencia continuaban compilando sin warnings ni errores visibles. El payload malicioso vivía en el `build.rs` del crate inyectado, no en el código que los developers llamaban directamente. Esta separación entre superficie visible y superficie ejecutable es exactamente lo que hace al ataque efectivo: el developer ve compilar su código, ve pasar sus tests, ve producir binarios correctos — y no tiene ninguna señal inmediata de que algo va mal.

El anzuelo: yankings selectivos para silenciar el aviso

El mecanismo de entrega es lo que vuelve este ataque técnicamente notable. Para que la víctima no viera el warning de Cargo sobre versiones yanked, el atacante yankeó `arrayref` 0.3.5, 0.3.6, 0.3.7, 0.3.8 y 0.3.9 en el mismo minuto en que publicó la versión 0.3.10 maliciosa. El resultado: la versión 0.3.10 quedaba como la única resolución válida para cualquier requirement de caret range sobre 0.3.x — y `arrayref` se declara típicamente con `^0.3`, que acepta cualquier versión 0.3.x.

Como reportó el investigador que descubrió el ataque en producción (GitHub: jhobern): «0.3.5 a 0.3.9 están todos yanked bajo la cuenta del owner, así que el warning de Cargo sobre consider updating to a non-yanked version es el cebo. Así es como me lo encontré.» El atacante invirtió la lógica defensiva de Cargo contra el developer. La ironía operativa es perfecta: la misma herramienta que la comunidad diseñó para alertar sobre versiones riesgosas fue usada como vector de ingeniería social.

El Hacker News verificó la cadena de dependencias nombrada en el reporte contra el índice de crates.io el 21 de agosto: `winit` requiere `sctk-adwaita ^0.10.1`, que requiere `tiny-skia ^0.11`, que requiere `arrayref ^0.3.6`. Cada requirement es caret range sobre 0.3.x, y caret sobre 0.3.x acepta 0.3.10. La cadena se mantiene en producción. Lo mismo aplica a blake3, que declaró `arrayref` como dependencia hasta la versión 1.8.6 y la eliminó en la 1.8.7, publicada a las 09:09 UTC del 20 de agosto — apenas cuatro minutos después de que el atacante publicara la versión maliciosa y cincuenta y tres minutos antes de que fuera borrada.

Qué hace el payload durante el build

El `build.rs` de `proc-macro1` reconstruye su payload y la dirección de command-and-control a partir de fragmentos en base64 en tiempo de compilación — no hay strings estáticas que un escaneo AV ingenuo detecte. Esta técnica de obfuscación por fragmentación no es nueva, pero sigue siendo efectiva porque los scanners de endpoint analizan strings literales y secciones de datos, no el resultado de operaciones de concatenación dinámica en tiempo de build. La firma del binario cambia con cada build, lo que hace que las técnicas tradicionales de hash-based detection fallen.

Luego instala un verificador de certificados TLS custom cuyos tres métodos de verificación retornan éxito incondicionalmente, desactivando la validación TLS. Esta es una decisión de diseño deliberada y agresiva: sin TLS válido, el stage-2 puede recibir instrucciones de un C2 que el atacante rota arbitrariamente sin necesidad de comprometer una CA real. El atacante intercambió confidencialidad en tránsito por flexibilidad operativa.

Después selecciona uno de cuatro payloads según sistema operativo y arquitectura de CPU. En Unix y macOS escribe los bytes a `/tmp/rust-setup`, marca el archivo ejecutable y lo lanza detached con la dirección de C2 como primer argumento. En Windows escribe un script PowerShell en `%TEMP%` y lo lanza oculto a través de un launcher VBScript bajo `wscript.exe`, abandonando el proceso hijo. El comentario en el source —capturado por los investigadores— dice textualmente: «escaping Cargo's job object so the build does not wait on it». Cargo, por defecto, espera a que termine cada child process; el atacante entendió ese comportamiento y construyó un bypass. El detalle técnico es importante: si Cargo esperara al child process, un timeout o un error visible alertaría al developer. Abandonando el proceso, el atacante garantiza que la build termina «exitosamente» y que el developer no ve nada raro en el output de Cargo.

El implant de segunda etapa hace beacon por HTTPS POST al path `/49890878` y se persiste de tres formas distintas según plataforma: clave de Registry Run en Windows, LaunchAgent en macOS, servicio systemd de usuario en Linux. La elección de tres mecanismos distintos según plataforma no es accidental: refleja un entendimiento profundo de cómo cada sistema operativo detecta persistencia y de qué artefactos un hunter esperaría ver. Soporta cuatro comandos: terminación, reconfiguración de C2, instalación de persistencia, y descarga y ejecución de scripts adicionales. El análisis de Wiz Research, publicado el día del incidente, confirma solapamiento significativo con campañas atribuidas a actores vinculados a Corea del Norte — específicamente en patrones de ofuscación, infraestructura de C2 y elección de targets de supply chain.

Por qué este caso importa más allá de Rust

Tres detalles hacen de este incidente un parteaguas, no un CVE más.

Primero, la superficie es masiva. Wiz Research estima que los paquetes impactados están presentes en el 35 % de los entornos cloud y de código del mundo, y en más del 75 % de los entornos que usan Rust. El número no es teórico: arrayref es dependencia transitiva de crates tan comunes como blake3 (que ya droppeó la dependencia en su versión 1.8.7, publicada a las 09:09 UTC del 20 de agosto), `blake2b_simd` y `blake2s_simd` (que la dropearon en releases publicadas entre las 09:25 y 09:26). La velocidad con la que el ecosistema se movió para cortar la cadena sugiere que la superficie era real y conocida. Un ecosistema más lento habría significado semanas de exposición.

Segundo, la ventana entre disclosure público y remediation sigue siendo inaceptablemente larga. Los yankings ocurrieron en una hora, pero cualquier build que ocurrió entre las 07:15 y las 09:25 UTC — y cualquier build incremental posterior que reusara el `target/` cache — está comprometida de forma silenciosa. Cargo no invalida el `target/` automáticamente cuando cambia el lockfile de dependencias maliciosas; el developer debe correr `cargo clean` manualmente. Esta decisión de diseño de Cargo es razonable para builds normales — los artifacts en `target/` son reproducibles a partir del lockfile — pero se vuelve peligrosa cuando el lockfile mismo contiene un payload malicioso. El developer que reusa un `target/` cache contaminado está, sin saberlo, redistribuyendo binarios comprometidos a staging y producción.

Tercero, el mecanismo de build-time execution evade los modelos de threat modeling que las empresas tienen hoy. SBOM tools reportan `arrayref 0.3.10` como dependencia resuelta — y es cierto, lo es — pero no reportan que `proc-macro1` también entró al lockfile. El modelo estándar de «lista de dependencias» se queda corto cuando el ataque vive en un crate que no debería estar ahí. Peor aún: el atacante publicó `proc-macro1` con un nombre que parece legítimo. Un SBOM que enumera dependencias directas no marca la diferencia entre `proc-macro2` y `proc-macro1` a menos que el tool tenga un dictionary de typosquats conocidos — y los typosquats son infinitos.

Qué hacer ahora si usas Rust en producción

Si tienes builds de Rust en CI que pudieron resolver las versiones maliciosas, asume compromiso. El orden de operaciones importa.

Primero, audita tu `Cargo.lock` por las versiones específicas: `arrayref` 0.3.10, `internment` 0.8.7, `append-only-vec` 0.1.9, y cualquier versión de `proc-macro1`, `proc-macro-en`, `aovine`, `arone`, `aronenao` o `tinymember`. La presencia de cualquiera de estos es un indicador de compromiso; no es un falso positivo que se pueda ignorar. El audit puede hacerse con un one-liner de grep sobre el lockfile:

```bash grep -E 'name = "(arrayref|internment|append-only-vec|proc-macro1|proc-macro-en|aovine|arone|aronenao|tinymember)"' Cargo.lock ```

Si cualquiera de esos nombres aparece, ese workspace estuvo expuesto durante la ventana. La siguiente pregunta es si la build se ejecutó contra la versión maliciosa — eso requiere correlacionar timestamps del CI con los timestamps de publish/borrado del advisory de RustSec.

Segundo, pinea `arrayref` a 0.3.9 o anterior en todos los `Cargo.toml` del workspace hasta que el Rust Security Response Team cierre completamente RUSTSEC-2026-0260. El equipo de RustSec está trabajando en una nueva versión de `arrayref` con el maintainer legítimo (David Roundy, account 2402, registrado en octubre de 2009), cuya cuenta sigue bloqueada mientras se investiga la intrusión. Para pinear:

```toml [dependencies] arrayref = "=0.3.9" ```

El operador `=` (no caret) garantiza que ninguna resolución futura escape al pin hasta que un humano lo revise.

Tercero, limpia el `target/` cache de cada máquina de CI y de cada developer: `cargo clean` en cada workspace, y eliminación del `~/.cargo/registry/cache` donde Cargo guarda los crates descargados. El payload es build-time, pero el binario compilado que produce puede contener una de las cuatro variantes de stage-2. La limpieza del cache de registry es importante porque el binario de `proc-macro1` queda ahí incluso después de `cargo clean` — Cargo lo re-descargará en el próximo build, pero hasta entonces es una fuente potencial de re-infección si el `target/` se restaura de un backup.

Cuarto, busca indicadores de post-explotación. Registry Run keys nuevas en Windows, LaunchAgents desconocidos en macOS, servicios systemd de usuario nuevos en Linux. Cualquiera de estos en una máquina que compiló Rust entre el 20 y el 25 de agosto es un indicador de compromiso real. En Linux:

```bash systemctl --user list-units --type=service --state=running ls -la ~/.config/systemd/user/ ```

En macOS:

```bash launchctl list | grep -v 'apple\|com.apple' ls -la ~/Library/LaunchAgents/ ```

En Windows, buscar claves en `HKCU\Software\Microsoft\Windows\CurrentVersion\Run` y `HKLM\Software\Microsoft\Windows\CurrentVersion\Run` con timestamp de creación posterior al 20 de agosto.

Lo que cambia en cómo pensamos supply chain

El incidente demuestra tres cosas que la industria lleva años postergando.

La primera es que los ataques build-time son una clase de amenaza distinta a los ataques runtime. SBOM y SCA miran el código que se ejecuta; no miran el código que se compila. La diferencia es operacional, no teórica: una build comprometida produce binarios que pasan todos los análisis estáticos posteriores, porque el análisis estático opera sobre el output, no sobre el proceso. Esto invalida una assumption central de los pipelines de seguridad modernos: que un binario producido por una build reproducible es equivalente a un binario auditado. Después del 20 de agosto de 2026, esa assumption requiere matización.

La segunda es que las defensas perimetrales del maintainer son insuficientes. David Roundy mantiene `arrayref` desde 2009 sin incidentes reportados. La cadena de custodia de su account fue el eslabón débil. 2FA obligatoria, hardware keys para maintainers de crates con más de 100.000 descargas, rotación periódica de tokens de publish — ninguna de estas medidas es optativa después del 20 de agosto de 2026. La pregunta que cada registry debería responder hoy es: ¿cuántos maintainers en nuestro top-100 por descargas tienen 2FA habilitada con hardware key? Si la respuesta no es «100 %», hay una lista de remediation priorizada para el próximo trimestre.

La tercera es que el ecosistema respondió correctamente. El descubrimiento por Nextron Systems, el reporte rápido al Rust Security Response Team, la divulgación al researcher jhobern que confirmó el ataque en producción, el borrado en menos de dos horas, y la publicación transparente del advisory con timestamps exactos — el playbook funcionó. Lo que falló es la prevention upstream; el incident response fue notable por su velocidad y transparencia, no por su improvisación. Los advisories iniciales de RustSec usaron una estimación redondeada como placeholder mientras la remediación estaba activa; una vez extraídos los timestamps exactos de los logs de crates.io, cada advisory fue actualizado con la cifra específica por crate: 86, 90 y 107 minutos respectivamente.

El próximo ataque build-time no será a un crate de Rust. Será a npm, a PyPI, a Maven Central, o a un registry menos maduro que no tenga un equipo de respuesta articulado. La pregunta que cada organización debería hacerse hoy no es «¿podemos detectar este ataque?» sino «¿cuánto tarda nuestro incident response en contener un compromiso de nuestro registry primario, en horas, no en días?». La diferencia entre 86 minutos y 8 horas de ventana es la diferencia entre un incidente técnico y una brecha de datos.