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

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.

Nada en la instalación. Todo en el primer uso.lo que hace el desarrolladornpm installningún script de ciclo de vidatodas las comprobaciones pasannode_modulesel código está ahí, inertedurante minutos o semanasnpm testel agente ejercita el códigolas filas entran en el árbol.set(100, …)el disparador es un datoproceso Node desacopladolo que hace luego el cargadorperfilar la máquinahostname, arquitectura,CPU, memoria, uptimehacia Slack y Telegramconsultar el contratoun contrato inteligente enEthereum Sepolialeído por RPC públicoabrir la etapa dosintercambio de claves X25519,AES sobre el secreto ECDHcarga guardada en el contratolimpiarborrar los archivos,quitar el disparadordel prototipoLos tres destinos de salida son toda la campaña. Quítalos y el cargador es un proceso que despertó en una máquina sin nada.
Las defensas de instalación no ven nada, porque en la instalación no pasa nada. El cargador está en el propio método set de la biblioteca y se dispara cuando llega una clave con valor 100 — un dato corriente en una biblioteca de estructuras de datos. Genera un proceso Node desacoplado, informa de la máquina a un canal de Slack y a un chat de Telegram, luego lee su siguiente dirección en un contrato inteligente de Ethereum Sepolia, descifra una segunda etapa guardada en el contrato, y borra tanto sus archivos como el disparador.

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.

en un portátil de desarrolladorel cargador despierta como túGH_TOKEN · ~/.git-credentials · ~/.awstokens reales, host real, red realel perfil que envía describe tu Macnada entre el proceso e internetSlackllega el perfilTelegramllega el perfilRPC Sepoliallega la etapa dosTres destinos que parecen tráfico SaaS corriente.Después el cargador se borra.en un espacio de trabajo Bromureel cargador despierta en el invitadoghp_ + 36 chars — un marcador, no un tokenlos secretos reales siguen cifrados en el Macel perfil que envía describe una VM Ubuntuel proxy es la única salidacortafuegos de salida — no emparejado: Denyapi.telegram.org:443 tcp — bloqueadoEmparejado en el conmutador por IP y nombre DNS, yde nuevo en el proxy por nombre TLS y método.
El mismo cargador, dos entornos. En un portátil perfila tu máquina real, corre con tus tokens reales, y alcanza tres destinos que parecen tráfico SaaS corriente. En un espacio de trabajo Bromure perfila una VM, encuentra solo credenciales marcadoras que no valen nada aguas arriba y que activan el detector de compromiso si se sacan de su ámbito, y se topa a la salida con un conjunto ordenado de reglas cuyo valor por defecto para el tráfico no emparejado puede ser Deny.

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.