Ataque «arrayref» a la cadena de suministro y el «typosquat» de «proc-macro1»

Ya están disponibles los parches de php2shell (CVE-2026-109618 y CVE-2026-109619) para PHP en fin de vida útil. Dado que las respuestas del departamento de ventas están tardando más de lo habitual debido a la elevada demanda, la forma más rápida de obtener los paquetes parcheados es comprar ELS para Runtimes en línea (se abre en una pestaña nueva). Lee las preguntas frecuentes sobre los parches

Ir directamente al resumen generado por IA de esta entrada del blog

  • Resume esta entrada con Perplexity
  • Resume esta publicación con Gemini

La versión resumida

Ir a la versión completa ↓

Qué: El 20 de agosto de 2026, un atacante utilizó la cuenta de crates.io de un mantenedor legítimo para publicar versiones maliciosas de arrayref 0.3.10, append-only-vec 0.1.9 e internment 0.8.7. El código de las bibliotecas en las tres versiones estaba limpio. Cada una de ellas añadía una dependencia de «proc-macro1», un typosquat de «proc-macro2» de David Tolnay, cuyo archivo «build.rs» reconstruía direcciones del atacante a partir de base64, descargaba un binario de segunda fase a través de TLS sin comprobar los certificados y lo ejecutaba durante la compilación.

Impacto: «arrayref» cuenta con 245 millones de descargas totales en crates.io, de las cuales unos 53,7 millones se han producido en los últimos 90 días, y 403 crates lo incluyen como dependencia directa. La mayoría de los equipos nunca lo han seleccionado, ya que se integra en bibliotecas de criptografía, gráficos y blockchain. El análisis de StepSecurity sitúa la ventana de exposición de arrayref en 86 minutos, desde su publicación a las 07:15 UTC hasta su eliminación del índice a las 08:41 UTC. append-only-vec e internment son más pequeños, con unos 4,5 millones y 14,4 millones de descargas totales, respectivamente, y desaparecieron en la misma oleada.

Estado: crates.io ha eliminado seis crates creadas por el atacante (proc-macro1, proc-macro-en, aovine, arone, aronenao, tinymember), ha retirado las tres versiones maliciosas y ha bloqueado la cuenta del responsable del mantenimiento. El Equipo de Respuesta de Seguridad de Rust afirma que «es probable que el ordenador o las credenciales del autor se hayan visto comprometidos». La limpieza del registro no elimina los archivos de tus equipos. Las cachés de Cargo, los árboles de dependencias de terceros, las capas de contenedores, las imágenes de CI y los servidores espejo aún pueden contenerlos. Si un host ha resuelto esa misma mañana un nuevo gráfico de dependencias que pudiera haber llegado a uno de estos crates, considéralo sospechoso hasta que lo compruebes.

La versión larga

¿Me afecta esto?

Aquí hay dos cuestiones importantes, y tienen respuestas diferentes. El plazo para adquirir las versiones maliciosas se cerró cuando crates.io las eliminó esa misma mañana. El plazo para ejecutarlas, en cambio, no. La retirada y la eliminación no borran nada de lo que ya está en el disco, por lo que una caché de Cargo, un árbol de dependencias de proveedores, un espejo del registro o una capa de contenedor que conservara una de estas versiones aún podría incorporarla a una compilación posterior. La adquisición tenía un plazo limitado. La ejecución es una propiedad de tus máquinas.

En el momento de la adquisición, el grupo afectado es más reducido de lo que sugieren las cifras de descargas. Un archivo Cargo.lock confirmado fija las versiones exactas, y Cargo mantiene las dependencias bloqueadas en las versiones del archivo de bloqueo siempre que puede, por lo que un proyecto compilado a partir de un archivo de bloqueo sin modificar se mantuvo en arrayref 0.3.9 y nunca pasó a 0.3.10. Esto también se aplica en un contenedor limpio. Un sistema de archivos nuevo no cambia nada si la compilación copia un archivo de bloqueo válido, que es lo habitual.

Lo que realmente puso en riesgo a los equipos fue la re-resolución mientras las versiones maliciosas estaban activas, en un grafo de dependencias que podía llegar a uno de los crates afectados. Cualquiera que ejecutara «cargo update» en esa situación quedó expuesto, al igual que cualquiera cuyo bot de dependencias abriera y fusionara una solicitud de extracción de actualización esa misma mañana. Lo mismo ocurrió con cualquiera cuya compilación se ejecutara sin el archivo de bloqueo presente, lo cual constituye el verdadero riesgo para los contenedores, ya que un «Dockerfile» que solo copie el manifiesto, o un «.dockerignore» que excluya «Cargo.lock», realiza la resolución desde cero cada vez. Lo mismo ocurre con cualquiera cuyo manifiesto cambiara esa mañana e introdujera una nueva ruta a `arrayref`, ya que Cargo vuelve a resolver la parte afectada del gráfico y escribe un nuevo archivo de bloqueo. Y cualquiera que ejecutara «cargo install», que ignora un archivo de bloqueo publicado a menos que se le pase el parámetro `–locked`.

Los autores de bibliotecas estaban más expuestos que los de aplicaciones, aunque ninguna norma lo exigía. El equipo de Cargo retiró en agosto de 2023 la antigua directriz de que «los binarios incluyen archivos de bloqueo en sus commits, pero las bibliotecas no», y desde entonces «cargo new» tiene en cuenta el archivo Cargo.lock para las bibliotecas; sin embargo, muchos repositorios de bibliotecas son anteriores a ese cambio y siguen resolviéndose en tiempo real en la integración continua (CI). Además, el archivo de bloqueo no se transmite a las etapas posteriores del proceso. Tu archivo de bloqueo no afecta a los usuarios de tu crate, solo lo hace tu Cargo.toml.

Empieza por la caché local de Cargo. El Equipo de Respuesta de Seguridad de Rust ha publicado esta comprobación:

find ~/.cargo/registry/cache -type f -name'append-only-vec-0.1.9.crate'-o-name'arrayref-0.3.10.crate'-o-name'internment-0.8.7.crate'-o-name'proc-macro1-*.crate'-o-name'proc-macro-en-*.crate'-o-name'aovine-*.crate'-o-name'arone-*.crate'-o-name'aronenao-*.crate'-o-name'tinymember-*.crate' -print

A continuación, comprueba si tus archivos de bloqueo contienen las mismas versiones y si hay alguna entrada de «proc-macro1». Una línea de «proc-macro1» en un archivo Cargo.lock ya es un indicio en sí mismo, ya que «proc-macro1» fue creado por el atacante para esta campaña y se ha eliminado del registro.

En qué consiste el ataque «arrayref y proc-macro1»

La operación comenzó horas antes de la descarga de la carga útil. Según la cronología de StepSecurity, a las 01:17 UTC se creó una cuenta de GitHub llamada «dtolney», que se hacía pasar por David Tolnay, responsable del verdadero «proc-macro2». A las 01:25 UTC se creó una cuenta correspondiente en crates.io. A las 01:55 UTC, esa cuenta publicó proc-macro1 1.0.106, una copia limpia de proc-macro2 que contenía metadatos de autor falsificados. Esa versión no realizaba ninguna acción maliciosa. Permaneció allí durante más de cinco horas, acumulando un pequeño historial de registro y pareciendo un crate sin nada especial.

A las 07:11 UTC, la misma cuenta publicó «proc-macro1 1.0.107». Esa fue la versión utilizada con fines maliciosos. Cuatro minutos más tarde, a las 07:15 UTC, apareció «arrayref 0.3.10» en la cuenta del propietario legítimo, con una dependencia del «typosquat». A continuación, se publicaron «internment 0.8.7» a las 07:34 UTC y «append-only-vec 0.1.9» a las 07:37 UTC.

El informe del Equipo de Respuesta de Seguridad de Rust, publicado ese mismo día, describe que se recibió un aviso a las 07:15 UTC indicando que «proc-macro1» era malicioso, y señala que el popular crate «arrayref» se había vuelto a publicar recientemente y apuntaba a él. El mérito del descubrimiento inicial recae en el equipo de investigación de Nextron Systems GmbH. Por otra parte, un informante que utiliza el alias jhobern registró el problema en la base de datos de avisos de RustSec y afirma que también envió un correo electrónico al equipo de seguridad de Rust. Según la reconstrucción de StepSecurity, ese informe de RustSec se remonta a las 07:54 UTC, 39 minutos después de la publicación maliciosa, lo que sugiere que los dos informes describen vías de notificación diferentes en lugar de la misma.

La limpieza comenzó a las 08:03 UTC con la eliminación de proc-macro1. Desde entonces, el Equipo de Respuesta de Seguridad de Rust ha publicado las horas exactas de eliminación de los tres crates legítimos. arrayref 0.3.10 se retiró a las 08:41 UTC tras 86 minutos en línea, internment 0.8.7 a las 09:04 UTC tras 90 minutos, y append-only-vec 0.1.9 a las 09:25 UTC tras 107 minutos. El registro de crates.io muestra que las versiones correctas que el atacante había eliminado se restauraron entre las 09:15 y las 09:26 UTC en los tres crates.

Cómo el «Yank» se convirtió en el «Lure»

Los incidentes anteriores de npm no tenían nada que ver con lo que ocurrió a continuación. En el mismo minuto en que se publicó la versión 0.3.10, el atacante utilizó la cuenta del propietario para retirar todas las versiones recientes de «arrayref» mediante una ráfaga de comandos automatizados, con un intervalo de unos cuatro segundos entre cada uno. En Cargo, retirar una versión no la elimina. Los archivos de bloqueo existentes que ya hacen referencia a una versión retirada siguen compilándose, y el resolutor solo omite las versiones retiradas cuando selecciona versiones nuevas. El historial de versiones de crates.io registra que las versiones 0.3.5 a 0.3.9 fueron retiradas entre las 07:15:24 y las 07:15:40 UTC desde la cuenta del propietario, y posteriormente recuperadas entre las 09:15 y las 09:17 UTC por un miembro del equipo de crates.io. Lo mismo ocurrió con «append-only-vec» 0.1.7 y 0.1.8 a las 07:38 UTC y con «internment» 0.8.3 a 0.8.6 a las 07:37 UTC.

Esto convierte una primitiva de mantenimiento rutinario del registro en una herramienta de ingeniería social, y su alcance era lo suficientemente limitado como para que funcionara. La eliminación no notifica a nadie, y un proyecto que ya esté vinculado a una versión eliminada sigue compilándose con ella. La advertencia sobre la versión retirada aparece en cuanto Cargo consulta el índice del registro, al ejecutar comandos como `cargo update`, `cargo add` o `cargo install` –locked, por lo que llega en primer lugar a las personas más propensas a volver a resolver la referencia. Para ellas, la versión maliciosa se convirtió en la versión más reciente que Cargo aceptaba y, para requisitos de compatibilidad habituales como ^0.3.5, era la única opción restante que no había sido retirada. Un desarrollador que viera la advertencia y ejecutara «cargo update» para eliminarla podría pensar, con razón, que estaba actuando de forma responsable.

Dentro del script de compilación

El código malicioso se encontraba en el archivo `build.rs` de `proc-macro1`, no en `arrayref`. Cualquiera que hubiera consultado el código fuente de `arrayref` 0.3.10 para comprobarlo habría encontrado las mismas macros de referencia a matrices que el crate siempre ha contenido, además de una línea de dependencia en `Cargo.toml`.

El script de compilación generaba los datos de comando y control a partir de fragmentos en base64, en lugar de codificar de forma fija una URL legible, y a continuación descargaba un binario de segunda fase desde https://23.254.165.112:9089/ a través de TLS, con la validación de certificados desactivada explícitamente. El descargador utilizaba rustls con un verificador de «aceptar todo» cuyos métodos de verificación devolvían un resultado satisfactorio de forma incondicional, por lo que la conexión estaba cifrada pero no ofrecía una autenticación efectiva del servidor. Se proporcionó a la carga útil una segunda dirección en el mismo host, 23.254.165.112:443, como punto final de comando y control. El nombre de host asociado, según StepSecurity, es hwsrv-798836.hostwindsdns.com.

La ejecución variaba según la plataforma. En los sistemas Unix, la carga útil se guardaba en /tmp/rust-setup, se le asignaba permiso de ejecución y se iniciaba como un proceso independiente. En Windows, se guardaba en %TEMP%\rust-setup.ps1 y se ejecutaba a través de %TEMP%\rust-setup-launch.vbs con wscript.exe y el parámetro CREATE_NO_WINDOW, de modo que no aparecía nada visible en pantalla.

Hubo dos razones por las que esto pasó desapercibido. En primer lugar, tal y como señala StepSecurity, el código de la biblioteca en «proc-macro1» es en realidad «proc-macro2», por lo que las compilaciones se realizan con éxito y no aparece nada extraño en la salida normal. En segundo lugar, ninguna parte de tu programa tenía que llamar nunca a una función arrayref, y no era necesario ejecutar cargo build específicamente. Cargo no tiene ningún gancho de instalación, por lo que no se ejecutaba nada en el momento de la descarga, pero un script de compilación se ejecuta justo antes de que se compile su paquete para cualquier comando que compile el grafo. Bastaba con cargo check, cargo test, cargo clippy y cargo doc, cada uno con el nivel de privilegios de quien o lo que los iniciara. En el portátil de un desarrollador, se trata de una sesión de usuario que puede acceder a claves SSH, credenciales de la nube y datos del navegador. En la integración continua (CI), se trata de un ejecutor que posee tokens de registro, material de firma y roles en la nube.

Durante las primeras horas, nadie podía decir qué hacía realmente el binario de la fase 2. Los informes de RustSec, StepSecurity y SafeDep documentaban el descargador y las direcciones de comando y control, y se quedaban ahí. Esa laguna ya se ha subsanado. Wiz Research ha analizado las cargas útiles recuperadas y describe una puerta trasera muy potente. Se comunica a través de HTTPS, envía los datos del host y las credenciales robadas en formato JSON codificado en base64, lee los datos de inicio de sesión guardados en las bases de datos SQLite de Chrome, Brave y Edge, e instala mecanismos de persistencia como servicio de usuario de systemd en Linux, como LaunchAgent en macOS o como clave de ejecución del Registro en Windows. Admite cuatro comandos, uno de los cuales recupera y ejecuta un shell o PowerShell arbitrario, y recurre a un algoritmo de generación de dominios si el C2 principal deja de responder. Por lo tanto, considera la ejecución confirmada como un compromiso del host y asume que se puede haber accedido a cualquier credencial a la que ese host o el ejecutor pudieran llegar, incluidas las contraseñas guardadas en un navegador.

La historia de la infraestructura también se volvió más interesante. La campaña Mastra comparable, llevada a cabo en junio, distribuía un programa de robo de información multiplataforma que se centraba en credenciales, datos del navegador y carteras de criptomonedas, y Wiz informa de que las cargas útiles «arrayref» envían señales a la misma ruta de solicitud C2 que utilizó Mastra, que la dirección de la señal comparte un emisor de certificados TLS con un servidor de Mastra, y que un C2 notificado por una víctima en este incidente aparece en la inteligencia de amenazas de Google relacionada con la campaña «axios» de un actor norcoreano. Microsoft atribuyó la actividad de Mastra a un grupo de la RPDC. Se trata de un solapamiento documentado de la infraestructura, más que de una atribución formal de este incidente, y es mucho más que el simple hecho de que dos campañas compartan un proveedor de alojamiento.

Por qué el modelo de compilación de Rust empeora aún más la situación

Rust goza de una sólida reputación en materia de seguridad, y se la ha ganado en aquellos aspectos del problema que el lenguaje aborda realmente. La seguridad de la memoria y un sistema de tipos estricto no impiden que una dependencia ejecute deliberadamente código arbitrario antes de que tu programa finalice.

Cargo ofrece a una dependencia dos formas de ejecutar código en tu equipo durante la compilación. La primera es build.rs. Al colocar ese archivo en la raíz de un paquete, Cargo compila el script y lo ejecuta «justo antes de compilar el paquete». Sus fines legítimos son reales y habituales, y abarcan la compilación de bibliotecas C, la generación de código y la detección de características, que es precisamente lo que hace que pasen desapercibidas en un diff. La segunda son las macros procedimentales, que se compilan y ejecutan dentro del compilador mientras se compila el código. La Referencia de Rust es clara sobre las consecuencias: las macros proc «plantean los mismos problemas de seguridad que los scripts de compilación de Cargo». Ninguna de las dos superficies está aislada en un entorno de pruebas.

El ecosistema de npm, al menos, ofrece la opción «npm install –ignore-scripts», que es la medida de mitigación señalada aquí tras el ataque de Mastra. Cargo no cuenta con ningún equivalente general ni en la versión estable ni en la nightly. No existe ninguna opción de compilación, clave de configuración ni opción -Z que simplemente desactive los scripts de compilación de dependencias arbitrarias y permita que el resto de la compilación continúe, y su aislamiento en un entorno de pruebas (sandbox) es un problema pendiente en Cargo desde 2018. La única opción de exclusión documentada es muy limitada. Un paquete que declare una clave «links» puede tener su script de compilación anulado en la configuración; en ese caso, el script no se compila ni se ejecuta. Esto se aplica a los crates -sys que proporcionan metadatos de bibliotecas C, pero no a un crate como proc-macro1. La razón más profunda por la que no existe una opción general es que un script de compilación no es una comodidad opcional en el momento de la instalación, como lo es un gancho «postinstall» de npm. Forma parte de la compilación, y desactivarlo suele significar que el paquete no se compila.

La distancia transitiva también es importante. «arrayref» es normalmente un crate de utilidades «leaf» que proporciona macros para obtener referencias a matrices, y sus versiones «clean» no declaran dependencias de tiempo de ejecución ni de compilación, solo una dependencia de desarrollo de «quickcheck». Eso es lo que hizo que la versión 0.3.10 fuera anómala, ya que «proc-macro1» fue la primera dependencia ordinaria que el crate había declarado jamás. Entre sus dependencias directas se encuentran tiny-skia-path, tiny-skia, libsecp256k1, multiaddr y schnorrkel, y la comprobación de dependencias inversas de StepSecurity añade revm-precompile en el lado de Ethereum y solana-runtime y spl-token en el lado de Solana. Una de las dependencias expuestas más importantes fue blake3, con aproximadamente 166 millones de descargas en total, que requirió arrayref ^0.3.5 hasta la versión 1.8.6 y luego se publicó la 1.8.7 sin ella a las 09:09 UTC de esa misma mañana. Por encima de esos crates hay muchos más proyectos que los utilizan. Casi nadie expuesto a este ataque tenía arrayref en su propio Cargo.toml. Tenían una biblioteca de gráficos o una biblioteca de criptografía, y esta llegaba por debajo de ella, dos o tres niveles más abajo, en un crate sobre el que no tenían motivos para opinar.

El aspecto que debería cambiar tu forma de ver la admisión

Todos los controles que, en teoría, podrían haber detectado este error actúan sobre algo distinto del producto final.

La fijación de versiones ayudó, y supone una protección real, pero es una protección basada en la inercia. Funciona hasta el momento en que necesitas una actualización de seguridad; en ese momento, resuelves el código nuevo e heredas lo que el registro esté sirviendo en ese instante. La firma y la autenticación de dos factores son controles de publicación valiosos, pero ninguno de ellos garantiza que el código que se publica sea inofensivo. El Equipo de Respuesta de Seguridad de Rust ha afirmado que es probable que el ordenador o las credenciales del mantenedor se vieran comprometidos, sin especificar públicamente cuál de los dos, por lo que aún no es posible determinar qué control de publicación habría evitado este incidente. Un análisis de composición basado en CVE no encontró ninguna coincidencia, ya que no existía ningún CVE durante el periodo de exposición. Un SBOM podría haber ayudado, pero solo como inventario. proc-macro1 se introdujo en arrayref como una dependencia ordinaria en lugar de una exclusiva de la compilación, por lo que un SBOM compatible con Cargo o derivado de un archivo de bloqueo generado antes de la compilación habría incluido proc-macro1 1.0.107 como un componente recién resuelto. Lo que no habría hecho es indicar que ese componente era malicioso. Un nivel más abajo, los crates que realmente llevaron a cabo la descarga —base64, rustls y ureq— eran dependencias de compilación de «proc-macro1», y un SBOM derivado de artefactos o centrado en el tiempo de ejecución podría omitirlos por completo.

La revisión del código fuente habría fallado al nivel de detalle al que operan la mayoría de los equipos, por la misma razón por la que falló en el ataque a Mastra. Un revisor que lea el código de la biblioteca de arrayref 0.3.10 encontrará macros limpias y familiares. El comportamiento malicioso se encuentra un nivel más abajo, en un script de compilación perteneciente a una dependencia que ningún miembro del equipo eligió, en un crate cuyo código de biblioteca es una copia de algo legítimo. Un revisor que leyera el diff completo de la versión, incluido el manifiesto, habría visto una nueva dependencia cuyo nombre difería en un dígito de «proc-macro2» y habría tenido un punto de partida. Esa es la profundidad de revisión que merece la pena institucionalizar, y no es la profundidad a la que operan la mayoría de los procesos de admisión.

Lo que distingue una posición defendible de una indefendible es el momento en el que se toma la decisión sobre la confianza. Si se deja para cuando la compilación ya haya comenzado, probablemente sea demasiado tarde, ya que el paquete se encuentra en el servidor de compilación y es posible que su script de compilación ya se haya ejecutado.

Mitigación

Sigue estos pasos en el orden indicado, ya que los tres primeros determinan si el resto del proceso consiste en tareas de limpieza o en la respuesta ante incidentes.

Busca los artefactos. Ejecuta la comprobación de la caché de Cargo indicada anteriormente en los equipos de los desarrolladores, los agentes de compilación y cualquier imagen de contenedor compilada el 20 de agosto de 2026. A continuación, busca en los archivos de bloqueo de todos los repositorios las referencias de matriz 0.3.10, append-only-vec 0.1.9, internment 0.8.7 y cualquier aparición de proc-macro1, proc-macro-en, aovine, arone, aronenao o tinymember.

Comprueba que coincidan con los hash conocidos. StepSecurity ha publicado los valores SHA-256 de los archivos comprimidos. El de arrayref 0.3.10 es 25ad700976873c76af785cb99b33c48db7df8b81f21d1e9e06b3676b9a9373ae, el de proc-macro1 1.0.107 es 61198155da51b838772eecf5bfaac6cbc4dcc388dccc56658fc28a8e831b34d4, y la versión limpia de señuelo de proc-macro1 1.0.106 es b5c1b5b0763a8809a644a8f92224653f0aca623a98eecc714d27f74b80fbe436. Limita la búsqueda a los tres. La versión 1.0.106 no contenía ninguna carga útil, por lo que encontrarla no indica que se haya ejecutado ni que haya habido contacto alguno con la propia infraestructura del atacante, pero es un indicio útil de que algo en el entorno resolvió el typosquat durante su fase de preparación.

Busca la ejecución, no solo la presencia. En los terminales, busca /tmp/rust-setup en los hosts Unix, y rust-setup.ps1 o rust-setup-launch.vbs en %TEMP% en Windows. Esos son los nombres locales que escribe el dropper. En la telemetría de proxy, red o descargas, busca por separado las solicitudes que incluyan nombres como rust-crate_0.1.0 hasta rust-crate_0.4.0, que son los nombres de archivo específicos de la etapa 2 que el descargador selecciona en el lado remoto, en lugar de los archivos que aparecen en el disco con esos nombres. Comprueba si se ha ejecutado wscript.exe a partir de un proceso de compilación y si hay un proceso hijo que haya sobrevivido a la etapa de compilación. La presencia de un archivo .crate en la caché indica que se ha descargado el archivo comprimido. Los artefactos del dropper en el disco indican que el script de compilación se ejecutó lo suficiente como para escribir la carga útil, y la telemetría de procesos o de red es lo que confirma que la propia carga útil se ejecutó. Si se sospecha que se ha producido una ejecución, busca también la persistencia que instala la puerta trasera, es decir, un servicio de usuario de systemd no reconocido en Linux, un LaunchAgent aislado en macOS o una entrada inesperada en «Ejecutar» de HKCU en Windows.

Bloquea y revisa el tráfico saliente. Añade 23.254.165.112 a tus listas de bloqueo, junto con 23.254.167.107 y 23.254.167.216, ambas identificadas como direcciones de comando y control de la fase 2, y no limites las reglas a los puertos observados 9089 y 443. Añade también hwsrv-798836.hostwindsdns.com y, a continuación, revisa retrospectivamente los registros del proxy y del cortafuegos en busca de conexiones procedentes de hosts de compilación, incluidas las solicitudes HTTPS a la ruta /49890878, que es el punto final de la baliza de la fase 2. Un proceso de compilación que llega a una dirección IP desnuda en un puerto no estándar es un evento de alta relevancia que merece la pena investigar, independientemente de esta campaña.

Analiza todo aquello a lo que la compilación pudiera acceder. Si la carga útil se ejecutó, limita el análisis a lo que poseía esa identidad, en lugar de a lo que hacía el crate. En el equipo de un desarrollador, esto incluye claves SSH, credenciales de la nube, tokens de registro, cualquier secreto expuesto a través del entorno del shell y los datos de inicio de sesión guardados en Chrome, Brave o Edge, ya que la carga útil los lee directamente. En la integración continua (CI), esto incluye secretos del ejecutor, relaciones de confianza OIDC, tokens de publicación de paquetes y material de firma.

Fija correctamente la dependencia transitiva. arrayref 0.3.9 está limpio, ya no está en «unyanked» y vuelve a estar disponible. Casi nadie tiene arrayref en su propio manifiesto, por lo que arrayref = «=0.3.9» implica añadir una dependencia directa que, de otro modo, no utilizarías, y una fijación exacta puede hacer que el grafo resulte insatisfacible más adelante. Para una dependencia transitiva, la opción más limpia es «cargo update -p arrayref –precise 0.3.9», con el archivo de bloqueo resultante confirmado y aplicado.

Haz que el cumplimiento de las normas del archivo de bloqueo sea obligatorio. Ejecuta la integración continua (CI) con la opción –locked, que indica a Cargo que genere un error en lugar de modificar el archivo de bloqueo, de modo que cualquier resolución inesperada provoque el fallo de la compilación en lugar de que se publique. Exige una revisión manual de las solicitudes de incorporación de cambios (pull requests) del bot de dependencias que modifiquen versiones transitivas, y considera cualquier aviso de actualización —ya sea una advertencia de «yank», una solicitud de incorporación de cambios del bot o un trabajo de CI con estado rojo— como un motivo para revisar el diff en lugar de volver a resolver las dependencias.

Detección

Observa qué hace la compilación. A las 07:11 UTC, ni las fuentes de CVE ni las bases de datos de vulnerabilidades ofrecían información sobre «proc-macro1», y los datos de reputación solo contaban con cinco horas de historial del registro con los que trabajar. El comportamiento era la señal disponible. Merece la pena considerar como relevante para la seguridad cualquier actividad de red saliente inesperada procedente de un script de compilación, y un proceso de compilación que escribe un ejecutable en un directorio temporal y lo separa es, por sí solo, un indicador claro.

Asegúrate de que el linaje del proceso sea el correcto al escribir la regla. Un script de compilación se ejecuta como su propio ejecutable compilado que genera Cargo, normalmente «build-script-build» en el directorio de destino, por lo que la señal llega a la red a través de un proceso hijo de Cargo de corta duración, y no a través del propio Cargo. Por el contrario, la carga útil de una macro procedimental se ejecutaría dentro de rustc. Genera una alerta en ambos casos, así como cuando aparezcan nuevos ejecutables en /tmp o %TEMP% durante una compilación.

En los hosts en los que se sospeche que se está ejecutando el código malicioso, amplía la investigación más allá del momento de la compilación. La puerta trasera de la fase 2 persiste a través de un servicio de usuario de systemd, un LaunchAgent o una clave «Run» del Registro, dependiendo de la plataforma, y envía señales a través de HTTPS, por lo que merece la pena alertar por sí sola de cualquier persistencia inesperada que se genere en torno al momento de la compilación.

En la capa de dependencias, presta atención a cualquier nueva dependencia que aparezca en la comparación de un archivo de bloqueo, especialmente aquellas cuyo nombre difiera en un carácter o un dígito respecto a un crate de uso generalizado. La diferencia entre «proc-macro1» y «proc-macro2» es la clave, y resulta evidente en la comparación si alguien revisa las diferencias de dependencias. Ten en cuenta que los crates de descarga en tiempo de compilación de esta campaña —base64, rustls y ureq— se declararon un nivel más abajo dentro de proc-macro1, por lo que una regla que solo inspeccionara tus propias dependencias directas de compilación no los habría detectado.

Cómo SecureChain para Rust cambia el resultado

La razón por la que este ataque funcionó es que la ejecución durante la compilación es un paso de confianza que no se somete a revisión en casi todos los flujos de trabajo de Rust. Esa es precisamente la brecha que SecureChain para Rust está diseñado para subsanar.

SecureChain recompila cada paquete desde el código fuente en entornos reforzados y aislados, y revisa el código fuente de los crates buscando específicamente patrones de ataque durante la compilación, incluidos scripts maliciosos de `build.rs` y macros procedimentales, bloqueando los crates afectados antes de que entren en tu cadena de herramientas. Esa distinción es más importante aquí que en un incidente típico de npm. Al no existir en Cargo un equivalente a –ignore-scripts, un equipo de Rust no puede desactivar los scripts de compilación, por lo que la revisión debe realizarse en la fase inicial, en lugar de desactivarse dentro del proceso de compilación. Un `build.rs` que recompone una URL a partir de fragmentos base64, desactiva la validación de certificados y descarga un binario desde una dirección IP sin más es el tipo de patrón en fase de compilación que la revisión pretende detectar, y revisarlo antes de que el crate llegue al portátil de un desarrollador es mucho más seguro que dejar que se ejecute primero e investigar después.

Descubre SecureChain para Rust →

SecureChain funciona como una alternativa verificada y recompilada desde el código fuente a la descarga directa desde crates.io, en lugar de ser un escáner acoplado al final del proceso. Los paquetes se analizan en busca de malware conocido, patrones de código sospechosos y riesgos en la cadena de suministro, y todo lo que no supere la inspección se bloquea antes de que llegue a tu entorno. Los riesgos específicos de Rust que señala son los que se han puesto de manifiesto en este incidente, entre los que se incluyen crates similares, editores comprometidos, versiones manipuladas, comportamientos ocultos y la apropiación de crates sin mantenimiento, en la que la propiedad de un crate abandonado se transfiere y luego se utiliza para distribuir una actualización maliciosa bajo un nombre que antes inspiraba confianza. Un crate cuyo nombre difiere en un solo dígito de una dependencia fundamental es exactamente el patrón de «crate similar» que un modelo de control de entrada está diseñado para tratar como sospechoso, y ese riesgo ya existía antes de que ninguna compilación lo utilizara.

Nada de esto cambia la forma de trabajar de los desarrolladores. SecureChain es un sustituto directo del repositorio, compatible con JFrog Artifactory, Sonatype Nexus y GitHub Packages, y los equipos conservan los mismos paquetes, las mismas API y los mismos comandos de Cargo. Además, ofrece información de procedencia según el nivel 3 de SLSA, SBOM y salida VEX. Un SBOM inventaría los componentes y las relaciones de dependencia dentro de su ámbito definido. La procedencia registra cómo y dónde se creó un artefacto, y proporciona pruebas sobre su origen y su proceso de creación.

Este problema tiene una ramificación más amplia que el «pinning» no puede resolver. «arrayref» es un crate de utilidades pequeño y estable, de esos que se escriben una vez y luego son utilizados discretamente por cientos de dependientes durante una década. Pocos equipos supervisan de cerca un crate como ese, y son muchísimos los que acaban incorporándolo a sus proyectos. Eso es lo que convierte a crates como este en objetivos atractivos, y lo que deja a los equipos atrapados entre una dependencia abandonada y una reescritura. SecureChain se encarga de supervisar y aplicar parches a los componentes compatibles, en el marco de un acuerdo de nivel de servicio (SLA), mientras se encuentren en cobertura activa. Cuando una versión alcanza el estado de fin de vida útil, el servicio «Endless Lifecycle Support» de TuxCare puede seguir proporcionando correcciones de seguridad para esa versión durante todo el tiempo que dependas de ella. Sin actualizaciones forzadas ni reescrituras.

Por qué esto va más allá de una simple campaña

Un ecosistema diferente, el mismo fallo. La campaña de Mastra de junio añadió una única línea de dependencia con typosquatting a más de 140 paquetes npm limpios. La oleada «Mini Shai-Hulud» afectó a 172 paquetes de npm y a un par de paquetes de PyPI, a pesar de contar con una procedencia válida de nivel 3 según el estándar SLSA, mientras que su continuación, «Megalodon», se saltó por completo la publicación de paquetes e inyectó masivamente flujos de trabajo de GitHub Actions en miles de repositorios para robar credenciales en la nube, claves SSH y tokens. En este caso, el mismo guion se traslada a Cargo, utilizando una cuenta de administrador comprometida, una dependencia con typosquatting y la propia semántica «yank» de Cargo para crear la presión de actualización. Desde entonces, Wiz ha documentado solapamientos en la infraestructura entre esta campaña y Mastra, y se ha atribuido Mastra a un clúster norcoreano, por lo que el ecosistema cambia, mientras que el operador puede que no.

En todos estos incidentes, la superficie comprometida variaba: podía tratarse de una dependencia transitiva, de un paquete malicioso respaldado por su procedencia o del propio flujo de trabajo de CI/CD. El error común consistía en considerar que un editor autenticado, una procedencia válida o una compilación que superaba las pruebas constituían pruebas de que el comportamiento observado durante la recepción y la compilación era benigno. Mini Shai-Hulud pone de manifiesto el límite de la procedencia, ya que sus artefactos maliciosos llevaban una procedencia SLSA criptográficamente válida. El comportamiento debe comprobarse en el momento de la entrada, antes de que comience la compilación, en lugar de en una máquina que ya contiene tus credenciales.

Descubre SecureChain para Rust →

PREGUNTAS FRECUENTES

¿Me protege un archivo Cargo.lock validado?

En cuanto a la obtención de la versión maliciosa, en gran medida sí. Un archivo de bloqueo sin modificar mantiene un proyecto en las versiones que ya se han resuelto, lo que para la mayoría de los equipos significaba arrayref 0.3.9, incluso en un contenedor limpio en el que se copia el archivo de bloqueo. Esa protección finaliza en el momento en que se resuelve una nueva versión, ya sea mediante «cargo update», un bot de dependencias, un cambio en el manifiesto que introduzca una nueva ruta al crate, una compilación que se ejecute sin el archivo de bloqueo presente o «cargo install» sin el parámetro «–locked». Además, tu archivo de bloqueo no llega a tus propios usuarios, ya que solo tu Cargo.toml determina lo que resuelven. Y nada de esto afecta a una versión maliciosa que ya se encuentre en una caché o en un árbol de dependencias integradas.

¿Puedo simplemente impedir que se ejecuten los scripts de compilación?

No de forma realista. No existe ninguna opción de Cargo, ni en la versión estable ni en la nocturna, que omita los scripts de compilación, y la única excepción documentada —que anula el script de compilación de un paquete que declare una clave «links»— no es aplicable de forma generalizada. A diferencia de los hooks de «npm install», los scripts de compilación de Rust forman parte del proceso de compilación y, a menudo, son imprescindibles para producir un artefacto funcional. Por eso, la inspección debe realizarse antes de la compilación, en lugar de desactivarse durante la misma.

¿Fue el responsable de mantenimiento el culpable?

El Equipo de Respuesta ante Incidentes de Seguridad de Rust ha declarado que no cree que el autor actuara con intención maliciosa y que la explicación más probable es que se trate de un equipo o unas credenciales comprometidas. La cuenta se ha bloqueado como medida de precaución. Se trata del mismo tipo de fallo que en el ataque a Mastra, en el que el punto de entrada fue la cuenta de npm de un antiguo colaborador que había permanecido inactiva durante unos 16 meses sin que se le hubiera revocado nunca el acceso.

¿Por qué era importante eliminar las versiones antiguas?

La acción de «yanking» no elimina una versión, y los archivos de bloqueo existentes siguen generándose. Lo que cambia es la resolución nueva, ya que el resolutor omite las versiones eliminadas a menos que ya estén en el archivo de bloqueo. Al eliminar las versiones correctas, la versión maliciosa quedó como la más reciente que la resolución nueva aceptaría. Cargo también muestra una advertencia de versión eliminada en cuanto se comunica con el índice, por lo que los desarrolladores que respondieron volviendo a resolver con «cargo update» fueron los que más probabilidades tuvieron de acabar con la versión 0.3.10.

¿Lo habría detectado un escáner de SBOM o de CVE?

No por sí mismas. No había ningún CVE que coincidiera mientras las versiones maliciosas estaban activas. Un SBOM generado antes de la compilación podría haber puesto de manifiesto la dependencia inesperada de «proc-macro1», ya que se declaró como una dependencia ordinaria, pero un SBOM es un inventario y no un detector de malware. Algunos SBOM también omiten las dependencias exclusivas de la compilación, que es lo que eran los crates del descargador dentro de «proc-macro1». La detección seguía requiriendo una política o una capa de análisis capaz de reconocer bien la nueva dependencia, bien su comportamiento.

¿Rust está más o menos expuesto que npm en este caso?

Rust presenta el problema de forma diferente. Cuenta con menos paquetes y una superficie de ataque menor en términos numéricos, y la disciplina en el uso de archivos de bloqueo durante el desarrollo de aplicaciones resulta de gran ayuda. Sin embargo, los scripts de compilación y las macros «proc» son más difíciles de evitar que los hooks de «npm install», por lo que la forma sencilla que ofrece npm de suprimir la ejecución de los scripts del ciclo de vida no tiene un equivalente directo en Cargo.

¿Cuál es la solución más importante a largo plazo?

Traslada la decisión sobre la fiabilidad a la fase de admisión. Recompila e inspecciona los paquetes antes de que entren en tu entorno, revisa las diferencias entre versiones, trata cualquier nueva dependencia como un cambio relevante para la seguridad y exige la procedencia como prueba del origen y del proceso de compilación, sin considerarla una garantía de que el comportamiento resultante sea inofensivo.

Únase a más de 4.500 profesionales de Linux y el código abierto.

2 veces al mes. Sin spam.

Índice
Obtenga las respuestas de seguridad de código abierto que necesita

Únase a más de 4.500 profesionales de Linux y el código abierto.

2 veces al mes. Sin spam.