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

Bastaba con estar conectado

Un desarrollador que estaba liberando espacio en disco encontró 700 MB en un directorio oculto y tiró del hilo: ZCode, de Z.ai, estaba metiendo en un tar todo su árbol de trabajo, 42.411 archivos con un 86,6 % de .git, lo cifraba con una clave que solo tiene Z.ai y lo publicaba directo en un bucket de Alibaba Cloud. Ningún atacante, ningún CVE, ningún malware. El disparador era tener la sesión iniciada. El cliente cifraba el paquete, así que ningún escáner podía leerlo, y los únicos datos que quedaban eran el destino y el tamaño. Esos son dos de los campos que Bromure Agentic Coding apunta para cada petición que hace un workspace, en un cable que la herramienta dentro de la VM no elige.

Su herramienta de programación no necesita estar comprometida para enviar su código fuente a algún sitio. Le basta con un ajuste por defecto. Confiar en el proveedor no es la cuestión. Pregúntese si se enteraría.

Está liberando espacio en un disco lleno. Un directorio oculto de su carpeta personal ocupa más de 700 megabytes, y no recuerda haber instalado nada de ese tamaño. Dentro, en una carpeta llamada checkpoints, hay un único archivo de 313 MB con extensión .enc y, al lado, un registro de metadatos que dice failureCount: 564.

Quinientos sesenta y cuatro intentos de algo que usted nunca pidió.

Setecientos megabytes y un hilo del que tirar

Ahí empezó el desarrollador chino conocido como ferstar, el 18 de septiembre, y el informe de ingeniería inversa de lo que encontró merece leerse entero. El directorio pertenecía a ZCode, el cliente de escritorio de programación con IA de Z.ai, la empresa detrás de los modelos GLM. CSO Online e InfoWorld recogen los dos su resumen del comportamiento palabra por palabra:

“Siempre que tiene la sesión iniciada, ZCode empaqueta en silencio todo su workspace — historial .git completo, caché de assets LFS, reflogs y configuraciones globales de la aplicación —, lo cifra y lo sube directamente a Aliyun OSS.”

El disparador está en la primera oración. No una pregunta que usted hizo, no la función de indexación que activó: un token de sesión válido, que el proceso de captura espera, y nada más. Una sola sesión registró 62 eventos de captura en sus logs. Cuando borró el paquete, el cliente construyó otro en menos de media hora e incrementó el contador de fallos.

Qué había en la caja

El snapshot que desmontó contenía 42.411 archivos, 345 MB antes de cifrar y 313 MB después. De qué se componían esos 345 MB:

  • .git/lfs/: 196,1 MB, 56,8 %
  • .git/objects/: 102,2 MB, 29,6 %
  • .git/logs/: 0,6 MB, 0,2 %
  • todo lo demás, es decir, código y documentación de verdad: 46,2 MB, 13,4 %

El 86,6 % de la subida era .git. El paquete llevaba cada objeto del historial del repositorio, cada binario grande que la caché LFS había descargado y el reflog, que enumera cada rama en la que ha estado y cada commit que hizo y luego deshizo. Quien lee su árbol de trabajo averigua qué está entregando; quien lee su .git averigua qué intentó, qué revirtió y quién lo commiteó.

Un token de sesión, cuatro pasos, ningún atacanteSu máquina, con sesióndirectorio de trabajo.git/lfs, .git/objects, .git/logscódigo, docs, configs globales42.411 archivos · 345 MBDos interruptores en Ajustesoptim. de experiencia → entrenamientoindexación de snapshot → índice servidorninguno controla el empaquetado1 · zcode.z.aidevuelve una firma OSSy una clave pública RSAla mitad privada queda en el servidor2 · empaquetado y sellado localtar.gz → AES-256-CTRclave ← RSA-OAEP-SHA256313 MB, ilegibles para usted3 · POST a Alibaba Cloud OSSbucket zcode-proddirecto — no por servidores de Z.aiun objeto por evento de captura4 · callback de OSSavisa al backend de que llegóel snapshot queda registrado eindexable en su ladoQué lo disparabaUn token de sesión válido. No un prompt, no una pregunta, no la función de indexación.62 eventos de captura en una sola sesión. Borre el paquete y se reconstruye en treinta minutos.failureCount: 564
Cómo salió el paquete de la máquina. El cliente ZCode pidió a zcode.z.ai credenciales de subida y recibió una firma de formulario OSS junto con una clave pública RSA; metió el directorio de trabajo en un tar, cifró el paquete localmente con AES-256-CTR bajo una clave envuelta con esa clave pública y publicó el resultado directamente en el almacenamiento de objetos de Alibaba Cloud, esquivando los propios servidores de aplicación de Z.ai, con un callback de OSS que avisaba al backend de que el objeto había llegado. La clave privada correspondiente nunca llega al cliente, así que el usuario no puede descifrar su propio paquete. Un token de sesión válido es el disparador, no ningún uso del asistente. Una sesión registró 62 eventos de captura, y borrar el archivo provocaba un nuevo empaquetado en unos treinta minutos.

Los dos interruptores gobernaban su lado

ZCode venía con dos ajustes que un desarrollador cuidadoso habría encontrado y apagado: uno para la optimización de la experiencia, otro para la indexación de snapshots del repositorio. Los dos hacían algo real, y ninguno hacía lo que su nombre sugiere.

El primero gobernaba si Z.ai estaba autorizada a usar los datos. El segundo gobernaba si el servidor indexaba el snapshot una vez llegaba. El empaquetado y la subida corrían de todos modos, porque el sidecar de captura arranca en cuanto el proveedor de tokens le entrega un JWT válido, sin ninguna preferencia de usuario en ningún punto del camino. Apague los dos interruptores y el cliente seguía metiendo su repositorio en un tar, seguía cifrándolo, seguía publicándolo en el bucket y prometía discreción sobre lo que pasaba después.

El fallo de un solo proveedor, pero la forma se repite en todas partes. Un control que vive dentro del programa que restringe es una petición. Corre sobre el mismo código que tendría que estar mal para que usted lo necesitara.

Un snapshot: 42.411 archivos, 345 MB antes de cifrar.git/lfs — 196,1 MB.git/objects — 102,2 MBcódigo — 46,2 MB56,8 %29,6 %13,4 %El 86,6 % de lo que salió de la máquina era el historial del repositorio — objetos, caché LFS, reflogsSolo el reflog ya es la lista de cada rama en la que estuvo y cada commit que hizo y luego deshizo.Los archivos de configuración global de la aplicación viajaron con él.
345 MB, y solo 46 de ellos eran código. El snapshot que ferstar recuperó contenía 42.411 archivos: 196,1 MB de caché de Git LFS, 102,2 MB de objetos de Git y 0,6 MB de reflogs, el 86,6 % de la carga, frente a 46,2 MB de código fuente y documentación de verdad. Una herramienta que sube su directorio de trabajo sube todo lo que usted ha escrito jamás en ese repositorio, incluidas las ramas que abandonó.

Nada en el cable podía leerlo

El cliente cifraba cada paquete con AES-256-CTR antes de que saliera de la máquina y envolvía la clave simétrica con una clave pública RSA-OAEP-SHA256 que le entregaba zcode.z.ai. La clave privada correspondiente nunca baja. ferstar no podía abrir su propio paquete; la única parte que podía era la que tenía el bucket.

Eso descarta toda defensa suya que funcione leyendo bytes. Un escáner de secretos no encuentra ninguna clave de AWS en ese tarball, y la inspección profunda de paquetes ve una sesión TLS hacia un endpoint de almacenamiento que transporta texto cifrado envuelto en texto cifrado.

Sobreviven dos datos: adónde fue y cuánto pesaba. Un POST de 313 MB a un host de almacenamiento de objetos en una región donde su equipo no despliega destaca, siempre que alguien apunte dónde se conectan sus herramientas y cuánto envían. Nadie lo hacía, y por eso encontrar esto exigió arqueología de espacio en disco y una semana de ingeniería inversa en vez de un vistazo a un log.

Dónde está la salida

Bromure Agentic Coding pone al agente y a su cadena de herramientas dentro de una VM Ubuntu en su Mac, y le da a esa VM exactamente una manera de alcanzar internet: un proxy man-in-the-middle que corre en el host, fuera del guest, donde ningún proceso de la VM puede reconfigurarlo.

Esto no pide nada al software. El conmutador de red virtual desvía de forma transparente los flujos de los puertos 80 y 443 de la VM hacia el proxy del host, sin ninguna variable de entorno necesaria y sin nada que el guest pueda desactivar, activo por defecto en todo workspace. Una herramienta que ignora HTTPS_PROXY, abre su propio socket y habla directamente con un endpoint de almacenamiento pasa igualmente por ahí, porque la interceptación vive en el conmutador al que el guest está enchufado y no en el guest.

Entonces le pasan cuatro cosas a un POST de 313 MB que nadie pidió.

Cada petición deja registro

Cada petición que pasa por el proxy deja un registro de traza: marca de tiempo, host, puerto, método, ruta, estado, latencia y bytes de petición medidos antes de cualquier canje de credencial. No necesita descifrar un paquete para advertir una subida del tamaño de un repositorio hacia un host donde nunca ha desplegado: el tamaño y el destino son columnas. Ordene por una, filtre por la otra.

Nunca sale del Mac

El cortafuegos de egreso por workspace es una tabla de reglas ordenada (acción, protocolo, host o CIDR, puertos, métodos HTTP) que termina en default allow o default deny. Ejecute un workspace en modo lista blanca y la subida no tiene adónde ir. Dos sitios aplican las reglas: el conmutador compara la IP de destino y el nombre de host escuchado por DNS, el proxy compara de nuevo el nombre de servidor TLS, y sus cambios llegan a las sesiones en marcha al instante.

Cada veredicto es una fila

Cada decisión del cortafuegos aterriza en la Security Timeline, así que una herramienta que sigue reintentando, 564 veces por ejemplo, aparece como una denegación repetida contra un host con nombre y no como un disco que se llena despacio. El patrón que costó una semana reconstruir es justo lo que esa ventana está hecha para mostrar.

Las configs que empaqueta son señuelos

“Configuraciones globales de la aplicación” es la expresión que más debería preocuparle de la frase de ferstar. Dentro de un workspace Bromure, ~/.git-credentials, ~/.docker/config.json, ~/.kube/config y ~/.aws/config existen y funcionan, y cada valor que contienen es un falso que preserva la estructura. Ningún archivo, variable de entorno ni proceso de la VM guarda jamás una clave de API, un token OAuth, un secreto de AWS o una clave privada SSH de verdad.

El alcance hace el resto. Un workspace es un proyecto, o una frontera de credenciales; dos workspaces nunca comparten una VM, y la carpeta personal del guest es su propio archivo de imagen. Lo que una herramienta empaqueta dentro de un workspace es ese workspace, lo que deja fuera los otros cuarenta repositorios de su Mac, el ~/.aws/config de su host y sus claves SSH. Las claves no llegan a ningún guest: la firma ocurre en el host por un puente vsock, y los bytes privados se quedan ahí.

La herramienta en el MacCarpeta personal~/work/* : cada repo, cada .git~/.aws/config, ~/.docker, ~/.ssh : realesLa herramienta elige qué leer y adónde enviarlo.La herramienta aplica sus propios ajustes.313 MB, cifrados, a un bucketNingún registro del destino. Ninguno del tamaño.Cómo se enteraSe le llena el disco y empieza la ingeniería inversa.La herramienta en un workspaceVM del workspace — guest Ubuntu, un proyecto/home/ubuntu — este repo, este checkout~/.aws/config, ~/.docker — placeholdersLa herramienta aún elige qué leer y enviar.No elige la ruta.:80 y :443 desviados al proxy del hosthost · método · ruta · bytes → trazadeny web *.aliyuncs.com · default denyRechazado en el cable y escrito en la Security Timeline.
La misma herramienta, dos ubicaciones. Ejecútela directamente en el Mac y se sienta al lado de cada repositorio que tiene, de cada archivo de credenciales de su carpeta personal y de una ruta sin mediación hacia internet; la subida es una sesión TLS como cualquier otra, y el paquete está cifrado, así que nada que lea contenido puede ayudar. Ejecútela en un workspace Bromure y sus flujos de los puertos 80 y 443 se desvían a un proxy del lado del host que no puede reconfigurar: el proxy registra el destino y el recuento de bytes, las reglas de egreso deciden si la petición sale, y los archivos de configuración que empaquetó guardan valores de relleno cuyos valores reales nunca entraron en la VM.

Qué hizo Z.ai después

Z.ai se disculpó el día en que ferstar publicó. La empresa desactivó el flujo de subida en ZCode v3.14.0, que según InfoWorld salió el 19 de septiembre listando una corrección para un problema anómalo de subida de repositorios, y llamó a la firma de seguridad NSFOCUS para auditar el resultado. NSFOCUS confirmó que “todos los objetos de datos del bucket zcode-prod de Alibaba Cloud OSS, así como el propio bucket, han sido eliminados”, y Z.ai dice que tras la auditoría “no se identificó ninguna ruta funcional capaz de disparar la generación de snapshots locales del repositorio ni de transmitir archivos locales al exterior”, y que el código “nunca se ha usado para entrenar modelos”. Después abrieron el código: cliente, backend, UI compartida, Agent CLI y runtime, con Apache-2.0, en GitHub. Eso supera la norma del sector, y significa que la próxima persona que quiera comprobarlo no necesitará un desensamblador.

Cris Thomas, de Semgrep, citado en la cobertura de CSO, nombró la categoría: “Esto no es realmente un problema del modelo de IA, es un problema anticuado de arquitectura de seguridad.” Quiere que los proveedores revelen qué sale de la máquina, adónde va y cuánto tiempo se conserva, “con los permisos mínimos activados por defecto, no los máximos”. Ningún cliente puede imponer eso desde fuera.

El ajuste por defecto es el modelo de amenaza

Aquí nadie atacó a nadie. Nadie robó una credencial, envenenó un paquete ni convenció a un modelo de nada. Un producto hizo lo que sus autores lo construyeron para hacer, y lo que lo construyeron para hacer fue enviar su repositorio a un sitio donde usted no podía leerlo. El incidente es un ajuste por defecto, y los ajustes por defecto los eligen personas cuyos intereses corren al lado de los suyos.

Ese es el argumento para poner la frontera fuera de la herramienta. No tiene que creer que su asistente de programación es hostil; solo tiene que notar que no puede auditar lo que no ve, y que el panel de ajustes del proveedor es un sitio raro para guardar el interruptor. Ejecute el agente y su cadena de herramientas donde cada conexión que abren cruce un cable que es suyo, donde el registro de ese cruce se escriba en su Mac, y donde la regla que detiene una subida de 313 MB sea una línea que usted escribió y no una casilla que honró otra persona.

Entonces “¿qué está enviando mi herramienta de programación?” es una ventana que abre un martes por la tarde, no un proyecto de investigación.