La instalación estaba limpia
En julio npm dejó de ejecutar por defecto los scripts de instalación de las dependencias. El 17 de septiembre Checkmarx publicó una campaña que no discute esa decisión: el cargador vive dentro de `BTree.prototype.set`, el método para el que existe la biblioteca, y se dispara la primera vez que algo guarda una clave con valor 100. Estuvo once semanas sobre dos millones de descargas semanales, y su canal de mando es un contrato inteligente en Ethereum Sepolia. Un agente de programación es la víctima ideal, porque ejecutar el código es el trabajo. Bromure Agentic Coding decide qué puede alcanzar ese código antes de que se ejecute.
Bloquear los scripts de instalación fue la decisión correcta, y el ecosistema npm la tomó en julio. Esta campaña trasladó su ejecución al primer uso del paquete: la instalación sigue limpia y es tu suite de pruebas la que aprieta el gatillo.
Tu agente necesita un mapa ordenado. Elige un paquete de árbol B en npm, lo
añade al package.json y lo instala. No se ejecuta nada. npm 12 ya no ejecuta
los scripts de instalación de las dependencias, así que no hay postinstall,
no hay preinstall, no hay hook de compilación, no hay nada que una política de
scripts de ciclo de vida pueda rechazar. El agente escribe el código que usa la
nueva dependencia, escribe una prueba que inserta unos cientos de filas, y la
ejecuta.
La centésima inserción arranca un segundo proceso Node.
Checkmarx publicó esta campaña el 17 de septiembre,
investigada por Bruno Dias.
BleepingComputer la cubrió el día 20
e InfoWorld el 21.
El paquete es indexed-btree, hecho para confundirse con el verdadero
sorted-btree, y cuando npm lo retiró el 3 de septiembre rondaba los dos
millones de descargas semanales. Llevaba en línea, en su forma maliciosa, unas
once semanas.
La defensa que todos acababan de terminar de construir
En junio la CLI de npm invirtió su valor por defecto para los scripts de
instalación, y
npm 12 entregó ese cambio en julio:
el preinstall, el install y el postinstall de una dependencia ya no se
ejecutan salvo que el proyecto los autorice por nombre, y las dependencias por
git o por URL remota también dejaron de resolverse por defecto. Las cifras de
JFrog, citadas en esa cobertura, sitúan esos tres vectores en cerca del 53 % de
los paquetes npm maliciosos vistos el año anterior. Los arneses de agentes
adoptaron la misma postura: --ignore-scripts, o un proxy que quita los hooks
del tarball antes de que el gestor de paquetes lo vea. Contando once semanas
hacia atrás desde la fecha de retirada, la primera versión maliciosa de
indexed-btree cae en mitad de ese despliegue.
Así que los operadores dejaron de usar scripts de instalación. El cargador vive
en BTree.prototype.set, el método para el que existe la biblioteca. Cuando la
clave que se le pasa es 100, el código lanza un cargador de primera etapa
ofuscado como proceso Node desacoplado, desde extended/sharedLoad.min.js. El
resumen de Checkmarx es exacto sobre por qué esto cuesta verlo venir: el
cargador se esconde dentro del propio método de la biblioteca, lo que es, en sus
palabras, “una forma bien construida de colarse ante las herramientas estándar
de análisis de contaminación”.
Fíjate en lo que lo arma: una clave con el valor 100, en una biblioteca cuyo propósito es guardar claves. Una tabla de clasificación llega ahí, un índice paginado llega ahí, y una prueba que inserta filas en bucle llega ahí en la centésima iteración. Usa el paquete como te dice su README y lo armas.
Once semanas, y un repositorio que se leía bien
El resto del oficio apunta a las comprobaciones que harías tú mismo.
El canal de mando es un contrato inteligente en Ethereum Sepolia. El cargador lee ahí su siguiente dirección, genera un par de claves X25519, toma del contrato la clave pública del operador, deriva una clave AES por ECDH, y descifra dos textos cifrados guardados en cadena para ensamblar la segunda etapa. Checkmarx es directo sobre la ventaja: “Esta técnica resiste mejor la retirada de dominios o IP que los enfoques C2 tradicionales, ya que usa el contrato inteligente como puntero a una nueva dirección cada vez que la anterior es desmantelada”. Nada de eso te da un dominio malo que meter en una lista de bloqueo. Obtienes un endpoint RPC público y una dirección de contrato, y los operadores pueden reapuntar el contrato sin tocar ni una sola copia instalada del paquete.
El proyecto parecía real. Los operadores montaron un repositorio de GitHub con un historial de commits plausible y una cuenta de desarrollador a juego, y el código de ese repositorio no contiene la parte maliciosa. Solo está en el tarball de npm. Leer el fuente en GitHub, que es el consejo que da todo el mundo, habría confirmado que el paquete estaba bien.
La antigüedad también jugó a su favor. Once semanas convierten un paquete en una
dependencia asentada para cuando lo instalas, nada que ver con la publicación
secuestrada que se retira en horas. Checkmarx contó nueve paquetes hermanos con
el mismo comportamiento, btree-core por sí solo por encima de 1,9 millones de
descargas y los demás en los cientos de miles cada uno, y dice que los
operadores controlan una cartera con 109 ETH.
Después, una vez asentada la segunda etapa, el cargador borra sus propios archivos y retira el disparador del prototipo. El paquete en disco vuelve a parecer una biblioteca de árbol B.
Ejecutar el código es el trabajo
Toda defensa anterior supone un momento en que una persona decide ejecutar algo. Un agente elimina ese momento.
Un agente de programación instala una dependencia, escribe código contra ella, lanza la suite de pruebas, lee el fallo, edita y vuelve a lanzarla, decenas de veces por hora, mientras tú estás en una reunión, porque ese bucle es toda la propuesta de valor. No estás leyendo la centésima inserción. El agente también tiene más motivos que tú para producir la clase de datos que activa este disparador: escribe fixtures en bucles, siembra tablas con identificadores secuenciales, y mide rendimiento con unos miles de filas. Si la condición de armado es “alguien guarda la clave 100”, un agente llega antes y más veces que una persona, en un proceso que sostiene lo que sostiene la sesión.
Las defensas de instalación cubren el momento en que llega un paquete. Tu agente pasa ahí un minuto al día y el resto ejecutando lo que llegó.
Lo que el cargador necesita
Reduce la campaña al hueso y necesita dos cosas: un sitio al que llegar, y algo que merezca la pena llevarse de la máquina en la que despertó. Tú decides ambas antes de que se ejecute, y ninguna de las dos decisiones exige reconocer el paquete.
En un portátil de desarrollador, las dos caen del lado del atacante. El
reconocimiento que envía — hostname, arquitectura, CPU, memoria y uptime —
describe tu máquina en tu red. El proceso que genera se ejecuta como tú, con tu
shell, tu ~/.git-credentials, tu GH_TOKEN, tu ~/.aws, tu agente SSH. Sus
tres destinos son slack.com, api.telegram.org y un proveedor público de RPC
de Ethereum: dos hosts SaaS con los que ya habla la mitad de tu instrumental, y
un endpoint de blockchain que parece infraestructura. Nada de esa lista se lee
como malicioso en un registro de netflow.
Bromure Agentic Coding responde a ambas mitades en el host, fuera de la VM en la que corre el agente.
Las credenciales van primero, porque son el objetivo. Cada secreto que
configuras existe dentro de la VM del espacio de trabajo como un marcador que
preserva la estructura (un token de GitHub es ghp_ más 36 caracteres, una
clave de Anthropic empieza por sk-ant-api03-brm-), derivado del valor real con
HKDF-SHA256 y una sal por instalación, de modo que gh, git y claude lo
aceptan y nunca parece rotar entre sesiones. Los valores reales permanecen
cifrados en tu Mac, y el proxy del host los sustituye en el cable después de que
la petición haya salido de la VM, solo para el host para el que se acuñó esa
credencial. El manual enuncia el invariante: ningún archivo, variable de entorno
ni proceso dentro de la VM contiene jamás una clave de API real, un token OAuth
real, un secreto de AWS real ni una clave privada SSH real. Un ladrón que lee el
entorno recoge marcadores.
Llevar un marcador a donde no le corresponde es, en sí mismo, la alarma. El proxy examina cada petición saliente en busca de falsos dirigidos a un host para el que no fueron acuñados. Cuando aparece uno, el proxy bloquea la petición con un 451, el destino no recibe nada, y Bromure pausa la VM. Reengancha a la fuerza una sesión desacoplada, tiñe el marco de rojo, y levanta una alerta que nombra la vista previa del token, la credencial, el host al que pertenecía y el host al que se dirigía. Puedes apagar el espacio de trabajo, o elegir Save for Investigation, que exporta el disco del sistema, el directorio personal y cada carpeta compartida antes de detenerlo. Este cargador borra sus propios archivos tras el apretón de manos, así que quien mire después encuentra un disco ordenado. Pausar a mitad de la petición te da la máquina antes de que ordene.
Después, el alcance. Las guardas traen un cortafuegos de salida por espacio de trabajo: un conjunto ordenado de reglas estilo pf sobre cada conexión que abre la VM, por host, CIDR, protocolo, puerto y, en las reglas web, por método HTTP individual, con un control Unmatched traffic puesto en Allow o Deny. Ponlo en Deny, lista los registros y las API que el trabajo necesita, y un cargador que estira la mano hacia una API de bot de Telegram o un proveedor de RPC de Ethereum se encuentra una negativa en el cable. Lo aplican dos capas, de forma que el invitado no puede rodear ninguna: el conmutador virtual empareja reglas por IP de destino y por hostname captado del DNS, en TCP y en UDP, y el proxy las empareja de nuevo por nombre de servidor TLS y por método. El conmutador desvía los flujos de los puertos 80 y 443 de la VM hacia el propio proxy, de modo que no hay variable de proxy que una carga útil pueda desactivar. Cada veredicto aterriza como una fila Firewall en la Security Timeline, y las ediciones de reglas llegan a las sesiones en curso sin reiniciar.
Donde el calendario no llega
El filtro de antigüedad de Bromure, activo por defecto en dos días, existe para el patrón del secuestro: una cuenta de mantenedor comprometida, una versión envenenada publicada, todo ello detectado y retirado en horas. Ha sostenido varias de estas historias. Once semanas superan cualquier corte que pusieras, de modo que aquí el trabajo recae en otras capas.
El panel Supply Chain es una pila por esa razón. La capa hecha para paquetes que fueron maliciosos desde el principio es el filtrado de paquetes: el bloqueo de paquetes comprometidos de socket.dev cubre paquetes marcados como malware, typosquats, scripts de instalación sospechosos y código ofuscado, y Depi sustituye el registro de npm por un espejo revisado de antemano. Ambos corren en el host, con sus claves de API guardadas en tu Mac y nunca exportadas a la VM, y ambos se sitúan delante de una instalación, no detrás. Un bloqueo vuelve como un HTTP 451 cuyo cuerpo en texto plano npm imprime palabra por palabra, así que el agente lee el motivo y lo rodea en vez de quedarse colgado.
Ningún servicio de reputación lo atrapa todo, y un typosquat que estuvo limpio once semanas es el caso difícil para todos ellos, y ahí es donde el resto de la pila se gana su sitio. Las credenciales marcadoras, el detector de compromiso y el conjunto de reglas de salida deciden qué puede alcanzar un paquete una vez instalado, y ninguno de ellos tiene que reconocer el paquete. Cubren código que nadie ha clasificado todavía, incluido el código que nadie clasificará durante otras once semanas.
Supón que se dispara en el primer uso
Bloquear scripts de ciclo de vida vale la pena y esta campaña lo rodea. Planifica para una carga útil que despierta cuando tu suite de pruebas llama a la biblioteca.
El disparador es un dato válido
Una clave con valor 100 en una biblioteca clave-valor. Ninguna lista blanca de entradas ayuda, y un agente genera ese dato más rápido que tú.
No dejes nada que robar en el proceso
Un reconocimiento que describe una VM y tokens que no valen nada aguas arriba convierten una ejecución lograda en una desperdiciada.
Decide los destinos por adelantado
Slack, Telegram y un endpoint público de RPC de Ethereum no son bloqueables por reputación. Una lista de permitidos no necesita reconocerlos.
Bloquear los scripts de instalación sacó del tablero cerca de la mitad de los
paquetes npm maliciosos. Por eso los operadores tuvieron que construir algo tan
elaborado — un repositorio falso, un disparador disfrazado de aritmética y un
canal de mando en una blockchain — para colocar código que hace un año
necesitaba cuatro líneas en un package.json. La defensa funcionó, y el precio
del ataque subió.
La suposición de debajo no se movió nunca. Sigues tratando una dependencia como segura una vez instalada, y ejecutarla como un acto neutro. Para un agente de programación, ejecutarla es el trabajo, miles de veces al día, sin nadie en la sala. Decide qué puede sostener ese proceso y adónde puede conectarse antes de que se ejecute, y la centésima inserción de tu agente acabará como una fila Firewall bloqueada en la Security Timeline.