Revocar el token era el detonante
El 8 de octubre, tensorlake 0.5.144 llegó a npm con un gusano Shai-Hulud dentro. Roba credenciales de GitHub, npm, la nube y SSH, coloca .claude/settings.json en los repositorios de la víctima e instala un monitor que comprueba el token de GitHub robado cada 60 segundos y ejecuta rm -rf ~/ en cuanto deja de funcionar. Revocar el token, el primer paso del protocolo habitual de respuesta a incidentes, borra el directorio personal de la víctima. En Bromure Agentic Coding, el filtro de antigüedad por defecto oculta esa versión a npm, y el token de GitHub que había en la máquina nunca fue el real.
El gusano dejó en su código una cadena para quien la encontrara: “IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner”. Si revocas este token, se borrará el ordenador de su dueño. El código hace lo que dice la cadena.
El miércoles 8 de octubre a las 01:12 UTC, el flujo de publicación de
tensorlakeai/tensorlake publicó la versión 0.5.144 del SDK tensorlake en
npm. Tensorlake es una plataforma serverless de sandboxes para agentes de IA, y
su paquete de npm tiene unas 12.000 descargas semanales. La nueva versión salió
del propio repositorio del proyecto, construida por el propio flujo del
proyecto, y npm mostraba para ella una atestación de procedencia.
Socket la marcó once minutos después. StepSecurity, Aikido y The Hacker News lo siguieron ese mismo día. La versión lleva una nueva variante del gusano Shai-Hulud, y esta viene con un rehén.
Cómo entró
StepSecurity reconstruyó la cronología. A las 01:20 UTC del 7 de octubre, alguien hizo push de un commit directamente a la rama principal del repositorio, con el nombre de un mantenedor. Siete commits más llegaron en las horas siguientes. Nadie los detectó, y un día después el flujo de publicación construyó el código de main y lo publicó. La atestación de procedencia era exacta. Demostraba dónde se había construido el paquete, y ese lugar ya estaba en manos del atacante.
El package.json del paquete ganó una línea:
"preinstall": "node lib/setup.mjs"
npm ejecuta preinstall antes incluso de desempaquetar el paquete en su sitio.
El script descarga el runtime Bun y lo usa para ejecutar lib/Math_Symbol.js,
una carga útil ofuscada de 856 KB. En CI no hace nada. El resumen de
StepSecurity: “el objetivo son las máquinas de los desarrolladores”.
Qué se lleva, y adónde va
La carga útil recoge tokens de GitHub y npm, claves de nube de AWS, Azure y
GCP, secretos de Kubernetes y HashiCorp Vault, claves SSH, archivos .env,
inicios de sesión guardados en el navegador, monederos de criptomonedas y la
configuración de herramientas de programación con IA: .claude, .cursor,
.kiro, Windsurf y Zed. Cifra el botín y lo envía a iseekaigogo.com o, si
eso falla, a un repositorio público que crea en la propia cuenta de GitHub de
la víctima, con la descripción “Shai-Hulud: Here We Go Again.” Cuando el
dominio cae, busca uno nuevo en un contrato de Ethereum a través de unos 30
endpoints RPC públicos.
Después se propaga. Con el token de npm de la víctima, descarga cada uno de sus
paquetes, se añade a sí mismo, sube la versión y vuelve a publicar. Con el
token de GitHub, hace commit de .claude/settings.json y .vscode/tasks.json
en los repositorios de la víctima, como [email protected], con
el mensaje chore: update dependencies. El siguiente desarrollador que abra uno
de esos proyectos en Claude Code o VS Code vuelve a ejecutar el gusano. Como lo
dijo Ashish Kurmi, de StepSecurity, el malware “se ejecuta de nuevo cuando
alguien abre el proyecto en Claude Code o VS Code”.
El rehén
Lo nuevo es un servicio llamado gh-token-monitor. En Linux es una unidad de
usuario de systemd en ~/.config/systemd/user/; en macOS, un LaunchAgent en
~/Library/LaunchAgents/com.user.gh-token-monitor.plist; y en Windows, una
tarea de inicio de sesión que ejecuta monitor.ps1. StepSecurity describe lo
que hace:
Cada 60 segundos, durante un máximo de 24 horas, el servicio comprueba ese token contra la API de GitHub. Si GitHub lo rechaza, el servicio ejecuta
rm -rf ~/.
Supón que eres la víctima. Te enteras de que un paquete que instalaste robó tu
token de GitHub. El protocolo habitual dice que lo revoques de inmediato. Lo
revocas, y en menos de un minuto tu directorio personal ha desaparecido: tus
copias de trabajo, tu trabajo sin commit, tus dotfiles y las pruebas de lo que
hizo el gusano. Socket encontró la amenaza escrita en el código como una
cadena literal: IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner.
Así que los avisos de seguridad vienen ahora con una regla de orden. El paso 3 de StepSecurity es encontrar y eliminar el monitor, y antes hacer copia de todo lo que no puedas perder. Su advertencia, en negrita: “No revoques ningún token de GitHub hasta haber terminado el paso 3.” Socket dice lo mismo. Cada hora que pasas buscando el monitor es una hora en la que el atacante tiene un token válido con tus permisos de push.
La misma instalación en una máquina de Bromure
En Bromure Agentic Coding, Claude Code, Codex y los demás agentes se ejecutan
en una VM Linux en tu Mac, una VM por máquina. Cada conexión que abre la VM,
incluida cada descarga de npm, pasa por un proxy en el Mac. Repite el 8 de
octubre en una de esas máquinas: el agente añade tensorlake a un proyecto y
ejecuta npm install.
La versión aún no existía
Cada máquina nueva tiene activado el filtro de antigüedad de la cadena de
suministro, con un mínimo de dos días. Cuando npm pide al registro la lista de
versiones de tensorlake, el proxy quita todas las publicadas hace menos de
dos días y hace que latest apunte a la más reciente que tenga la antigüedad
suficiente. El 8 de octubre, esa era la 0.5.143. npm la instaló y siguió
adelante. Desde dentro de la máquina, la 0.5.144 nunca existió, y para cuando
habría superado el corte ya había desaparecido de npm.
Si un lockfile o el agente hubieran pedido la 0.5.144 por su nombre, el proxy habría rechazado la descarga con un HTTP 451 y un motivo que npm muestra tal cual:
Bromure Supply-Chain Security blocked this request: npm package
[email protected] published 3 hours ago — policy requires 2 days minimum
El agente también lee esa línea, y fija la versión anterior por su cuenta.
Dos capas más están en el mismo panel Supply Chain. Con socket.dev
seleccionado y Block compromised packages activado, la descarga se rechaza
en cuanto socket.dev ha marcado el paquete, lo que a Socket le llevó once
minutos el 8 de octubre. Con Strip preinstall / install / postinstall /
prepare from npm tarballs on the fly activado, el proxy reescribe el tarball y
la línea "preinstall": "node lib/setup.mjs" desaparece antes de que npm la
vea. Esa línea es la única vía de entrada del gusano.
Supongamos que se ejecuta de todos modos
Quita todas las capas y deja que la carga útil se ejecute. Se ejecuta como el usuario de la VM, en el directorio personal de la propia máquina, y se pone a buscar lo que tiene en su lista.
Los tokens son sustitutos. Ningún archivo, variable de entorno ni proceso
de la VM contiene un token real de GitHub, un secreto de la nube o una clave
privada SSH. GH_TOKEN y ~/.git-credentials contienen un token ghp_ con la
forma correcta, y el proxy del Mac pone el real solo cuando una petición sale
hacia GitHub. ~/.ssh no tiene clave privada: SSH_AUTH_SOCK es un puente a
un ssh-agent en el Mac, que firma y nunca entrega la clave. El directorio
personal de tu Mac, su Llavero y sus perfiles de navegador no están en la VM en
absoluto. El gusano cifra y envía un lote de sustitutos que GitHub, AWS y
Kubernetes rechazan en cualquier sitio salvo a través de ese proxy.
La salida pasa por el Mac. Una máquina con Unmatched traffic en
Deny en su panel Guardrails, con reglas para su proveedor de modelos,
sus registros y GitHub, nunca se conecta a iseekaigogo.com ni a ninguno de
los 30 endpoints RPC de Ethereum. Sea cual sea la configuración, cada una de
esas conexiones aparece en la Security Timeline, siempre activa en todas
las máquinas.
Las escrituras esperan un clic. Las máquinas nuevas usan Prompt before
write para cada forja git. La vía de respaldo del gusano en GitHub crea un
repositorio público en tu cuenta: eso es un POST /user/repos, una escritura,
así que macOS muestra Allow write on "github.com" from workspace "…"? con la
petición tal como pasó por la red. Su propagación hace commit de
.claude/settings.json en tus repositorios y los empuja: el mismo diálogo,
esta vez con POST /acme/app.git/git-receive-pack. No le pediste al agente que
creara un repositorio llamado “Shai-Hulud: Here We Go Again.” Haces clic en
Don't allow, y la propagación del gusano termina en tu Mac.
El rehén es un sustituto
La unidad gh-token-monitor se instala igualmente en
~/.config/systemd/user/ de la VM y empieza a llamar a api.github.com/user
cada 60 segundos con el token que encontró. Ese token es el sustituto. El proxy
pone el real para GitHub, GitHub responde 200 y el monitor espera.
Eso invierte el orden de la respuesta. En un portátil, el token real está en
manos del atacante, y la carrera consiste en revocarlo antes de que lo use, sin
disparar el borrado. En Bromure, el token real nunca entró en la VM, así que el
atacante no tiene con qué competir contigo. Nada fuera de tu proxy acepta el
valor ghp_ que se llevó.
Apaga la máquina, y el monitor se detiene con ella. Abre Rewind home… y
elige el punto de control del arranque anterior a la instalación: Bromure toma
uno en cada arranque correcto. El archivo de la unidad, el script de
~/.local/bin y los archivos .claude/settings.json plantados en las copias de
trabajo de la máquina vuelven todos a como estaban. El directorio personal tal
como estaba un momento antes se guarda primero como nuevo punto de reversión,
así que el propio retroceso se puede deshacer. Para empezar de cero, Reset
disk vuelve a clonar el disco del sistema a partir de la imagen base firmada.
Y si el interruptor salta de todos modos, rm -rf ~/ borra /home/ubuntu
dentro de una sola VM. El directorio personal de tu Mac queda fuera de su
alcance, y Rewind home… recupera el de la máquina desde el último punto de
control.
¿Quién lo instaló?
La primera pregunta en un incidente como este es si alguna máquina del equipo descargó la versión mala. En un portátil, buscas en lockfiles, cachés de npm e historial del shell, y esperas que el gusano no haya borrado sus huellas.
En Bromure, cada descarga de paquete es una fila en la Security Timeline,
esté o no activada alguna capa de cadena de suministro:
npm · [email protected], con la máquina, la hora y la decisión. Las filas se
guardan 90 días en el Mac, donde nada dentro de una VM puede modificarlas.
Filtra por Supply chain, busca la versión y tienes la respuesta.
Export… la guarda como CSV para el ticket del incidente. En los Mac
inscritos en bromure.io, la misma descarga va al flujo de eventos de la
organización como un evento supply_chain.fetch, de modo que un equipo de
seguridad puede hacer la pregunta una sola vez para todos los desarrolladores.
La versión mala nunca apareció
El filtro de antigüedad, activado por defecto con un mínimo de dos días,
quita las versiones recientes de la lista de versiones de npm. El 8 de
octubre, npm install tensorlake resolvió a la 0.5.143.
El token robado era un sustituto
Las credenciales de GitHub, npm, la nube y SSH se quedan en el Mac. El
rehén del gusano era un valor ghp_ que solo funciona a través del proxy
de Bromure.
Propagarse requiere tu clic
Crear un repositorio y hacer push de un commit son escrituras. Con Prompt before write, el valor por defecto en las máquinas nuevas, ambos esperan un diálogo de macOS que la VM no puede responder.
El borrado afecta a una VM
rm -rf ~/ alcanza el directorio personal de la máquina, no el tuyo.
Rewind home lo restaura desde el punto de control del último arranque.
El próximo protocolo
Shai-Hulud ha vuelto una y otra vez este año, y sus autores diseñan cada versión según cómo los defensores manejaron la anterior. Los defensores revocaban tokens en cuestión de horas, así que esta castiga la revocación. La próxima castigará otra cosa: ejecutar un script de limpieza, borrar un LaunchAgent, volver a abrir el proyecto.
En un portátil, cada una de esas vueltas de tuerca funciona porque el atacante tiene algo real en la misma máquina que intentas limpiar. En una máquina de Bromure, las credenciales reales, tu directorio personal y el registro de lo ocurrido están todos en el Mac, detrás del proxy. Limpiar es retroceder, y puedes hacerlo antes de rotar nada. Para saber más sobre la frontera de red que hay detrás, lee La instalación estaba limpia y Cuarenta mil dólares por fuga.