← Volver a todas las publicaciones
Publicado el · por Renaud Deraison

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.

En un portátil: el primer paso del protocolo dispara el borrado1. npm install[email protected]preinstall: node lib/setup.mjsRoba los realestoken GitHub (push)token npm (publicación)~/.aws ~/.kube ~/.sshInstalagh-token-monitor (LaunchAgent).claude/settings.json en repos2. El monitor, cada 60 sGET api.github.com/userAuthorization: token real200 OK → sigue esperandoel atacante sigue teniendotus permisos de push401 → ejecuta el handler3. Revocas el tokencomo dice todo protocolorm -rf ~/copias de trabajo, cambios sincommit, dotfiles y las pruebas
En un portátil, el token robado es el real, así que revocarlo es urgente. El monitor convierte esa revocación en el borrado del directorio personal del desarrollador.

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.

En Bromure: la versión está oculta y el rehén es un sustitutoVM de la máquina (Ubuntu)$ npm install tensorlake+ [email protected]Si la carga útil se ejecuta igualmenteGH_TOKEN=ghp_… (sustituto)~/.ssh: sin claves privadassolo el home de esta máquinagh-token-monitor consulta GitHubcon el sustitutoun borrado alcanza /home/ubuntu en esta VM,y Rewind home lo recuperaProxy, en el MacFiltro de antigüedad: latest → 0.5.1430.5.144 por nombre → 451, con motivoCortafuegos: iseekaigogo.com → denegarEndpoints RPC de Ethereum → denegarcada veredicto en la Security TimelineDiálogo de macOS: Allow write on …?POST /user/reposPOST /acme/app.git/git-receive-packEl token real solo se pone para GitHubInternetregistry.npmjs.orggithub.comproveedor de modelosC2 del atacante:nunca contactadoGuardado en el Mac, fuera de la VMtoken real de GitHub · claves de nube · claves privadas SSH · tu carpeta personal · puntos de control · Security Timeline
La misma instalación en una máquina de Bromure. El filtro de antigüedad oculta la 0.5.144 a npm. Si la carga útil se ejecuta de todos modos, roba sustitutos, sus push y la creación de repositorios esperan un diálogo en el Mac, y el monitor rehén tiene un token que GitHub solo acepta a través del proxy de Bromure.

¿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.