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

La clave borrada seguía en el historial

El 8 de octubre, una campaña llamada GhostAction usó dos cuentas de mantenedores secuestradas para hacer commit de un workflow llamado security-audit.yml en 346 repositorios, entre ellos athenadriver de Uber y pyxel, con 18.000 estrellas. El 9 de octubre, Socket contaba más de 500 cuentas y decenas de miles de repositorios. El workflow ejecuta git log -p --all y busca con grep en cada commit jamás hecho claves de Anthropic, OpenAI, OpenRouter, GitHub y AWS, incluidas las que se borraron un commit después. En Bromure Agentic Coding, el agente solo tiene sustitutos, así que las claves que escribe en el historial de un repositorio no valen nada para quien las encuentre.

Borrar una clave de un archivo cuesta un commit. El commit anterior guarda la clave mientras exista el repositorio.

A las 13:40 UTC del jueves 8 de octubre, la cuenta de GitHub de Takashi Kitao, autor del motor de juegos pyxel, empezó a hacer commit de un archivo llamado .github/workflows/security-audit.yml. Cuatro minutos después estaba en 27 de sus repositorios. A las 21:10 UTC, la cuenta henrywoo, autor original de athenadriver de Uber, empezó el mismo barrido. A las 21:26 había llegado a 318 repositorios, entre ellos 279 forks y el propio uber/athenadriver, donde la cuenta tiene permiso de escritura.

Socket publicó su análisis el 9 de octubre y atribuyó los commits a GhostAction, una campaña de robo de credenciales vista por primera vez en septiembre de 2025. Al final de ese día, la actualización de Socket contaba más de 500 cuentas de GitHub que habían hecho commit del workflow en “decenas de miles de repositorios” desde el 7 de octubre. The Hacker News y StepSecurity cubrieron la misma ola. Socket comprobó que el workflow ya se había ejecutado en repositorios afectados, y que seguía en las ramas por defecto de kitao/pyxel y uber/athenadriver cuando se publicó el artículo.

Qué hace security-audit.yml

El workflow se llama “Security Audit”. Se ejecuta con workflow_dispatch y con cada push a cualquier rama o tag, sin filtro. Su paso de checkout usa fetch-depth: 0, que trae todas las ramas, todos los tags y el historial completo. Después, un único paso hace dos trabajos.

El primero va a por los secretos de GitHub Actions del repositorio. Antes de hacer commit del workflow, el atacante leyó los workflows existentes del repositorio y escribió en la carga útil los nombres de los secretos que usan. En pyxel eran CARGO_REGISTRY_TOKEN, PERSONAL_ACCESS_TOKEN, PYPI_PASSWORD y PYPI_USERNAME.

El segundo es nuevo en esta ola. El paso busca con grep 13 patrones de credenciales en el árbol de trabajo, luego ejecuta git log -p --all y busca los mismos patrones en hasta 200.000 líneas de historial:

  • Servicios de IA: Anthropic (sk-ant-), claves de proyecto de OpenAI (sk-proj-), OpenRouter (sk-or-)
  • Control de versiones: tokens clásicos de GitHub (ghp_), tokens de grano fino (github_pat_), tokens de GitLab (glpat-)
  • AWS: IDs de clave de acceso, IDs de clave temporal, claves secretas y tokens de sesión. El paso conserva además dos líneas de contexto alrededor de cada ID de clave, así que la clave secreta que se hizo commit a su lado también se va.
  • SaaS y nube: claves de API de Google, tokens de Slack, claves de SendGrid

Todo sale en un único POST de curl por HTTP sin cifrar a 193.32.204.199. Cada ejecución informa del nombre del repositorio, haya encontrado algo o no.

Socket señala a quién va dirigido. Dos de los repositorios de henrywoo son pyllama y chatllama, proyectos de aprendizaje automático cuyos colaboradores y autores de forks “probablemente tienen este tipo de claves”. Una clave de Anthropic u OpenAI se puede revender como inferencia facturable, y cada prompt que pasa por ella pasa por las manos del comprador.

El historial lo guarda todo

El consejo de remediación de Socket tiene una frase que debería preocupar a cualquiera que haya limpiado una clave filtrada editando un archivo: “Rota cada credencial encontrada en cualquier commit, esté o no todavía en el árbol actual”. Una clave eliminada en el commit siguiente sigue en el commit anterior. git log -p --all lee los dos.

Los agentes de código engordan ese historial deprisa. El informe State of Secrets Sprawl 2026 de GitGuardian encontró que los commits públicos coescritos por Claude Code filtraban secretos en un 3,2 % de los casos, frente a una base del 1,5 % en todo el GitHub público. El mismo informe contó 1.275.105 secretos filtrados ligados a servicios de IA en 2025, un 81 % más que el año anterior.

El patrón le resultará familiar a cualquiera que haya visto a un agente hacer pasar un test de integración que fallaba. El agente tiene la clave en su entorno. La escribe en un archivo de configuración, un fixture de test o un .env que todavía no está ignorado, porque así pasa el test. Hace commit. Más tarde, el desarrollador o el agente mueve la clave a una variable de entorno y vuelve a hacer commit. El árbol parece limpio. El historial tiene la clave, cada fork tiene el historial, y un workflow con fetch-depth: 0 lo lee.

El token que escribió el workflow

Socket no determinó cómo se tomaron las dos cuentas. The Hacker News señala la fuente más probable: un personal access token sacado de registros de infostealers o de volcados de credenciales. Los registros de infostealers salen de máquinas de desarrolladores. En un portátil, el token de GitHub está en ~/.config/gh/hosts.yml, en ~/.git-credentials y a menudo en GH_TOKEN, legible por todo lo que se ejecute como el desarrollador, incluido lo que un agente de código instale o ejecute.

Basta un token. Con él, el atacante puede listar los repositorios en los que la cuenta puede escribir, leer sus workflows para sacar los nombres de los secretos y hacer commit en cada uno. El barrido de henrywoo cubrió 318 repositorios en 16 minutos.

Un token robado, un workflow, cada commit jamás hechoPortátil del mantenedor~/.config/gh/hosts.yml~/.git-credentialsGH_TOKEN=ghp_… (real)log de infostealer→ token en ventafuente probable (THN)security-audit.yml: 318 repos, 16 minon: push (any branch), workflow_dispatchfetch-depth: 0git log -p --all | grep 13 patternsHistorial de config/test.pya1f3 + KEY="sk-ant-api03-…" ← halladac92e - KEY="sk-ant-api03-…" + KEY=os.environ["…"]Árbol limpio, clave aún en el historial.193.32.204.199POST (HTTP plano)REPO=…PYPI_PASSWORD=…sk-ant-api03-…AKIA_CTX_START…claves actualesy borradas por igualCada fork lleva el mismo historial. Socket contó 279 forks solo en henrywoo.
La cadena de GhostAction. Un token de una máquina de desarrollador hace commit del workflow, el workflow lee cada commit jamás hecho, y cada clave de ese historial, actual o borrada, sale en un único POST.

El mismo repositorio, escrito desde una máquina Bromure

En Bromure Agentic Coding, el agente se ejecuta dentro de una VM Linux en tu Mac, y cada petición HTTP que hace la VM atraviesa un proxy en el host. Tu clave de Anthropic, tu token de GitHub y tus claves de AWS se quedan cifrados en el Mac. Ahora suelta GhostAction sobre un repositorio que escribió una máquina Bromure, y sobre el desarrollador que lo escribió.

El historial guarda sustitutos

Dentro de la VM, cada credencial es un sustituto con la forma de la real: ANTHROPIC_API_KEY es sk-ant-api03-brm-…, una clave de OpenRouter es sk-or-v1-brm-…, GH_TOKEN y ~/.git-credentials contienen ghp_ más 36 caracteres. Cada uno se deriva del valor real y de una sal propia de la instalación con HKDF-SHA256, y no se puede convertir de vuelta en él. El proxy pone el valor real en el cable, y solo para una petición al host al que pertenece la credencial.

Así que, cuando el agente escribe su clave en config/test.py para que pase el test, escribe lo que tiene. Cuando hace commit, el commit contiene sk-ant-api03-brm-…. Cuando llega más tarde el commit de limpieza, el historial conserva ese mismo sustituto.

El grep de GhostAction lo encuentra. sk-ant- coincide con sk-ant-api03-brm-, y ghp_ coincide con un token de GitHub sustituto. El POST a 193.32.204.199 se los lleva, y el comprador los prueba. Anthropic rechaza la clave y GitHub rechaza el token, porque lo único que los convierte en credenciales válidas es el proxy de tu Mac, para peticiones que salen de tu VM. El consejo de Socket de rotar cada clave encontrada en cualquier commit tiene una respuesta corta para este repositorio: ninguna de esas claves funcionó nunca fuera de tu Mac, y los sustitutos de los proveedores de IA llevan -brm- en el nombre, así que puedes localizarlos al escanear el historial.

El token del portátil también es un sustituto

La otra mitad de la campaña es la propia cuenta. Ejecuta tus agentes en una máquina Bromure y el token de GitHub que usan se queda en el Mac. Todo lo que se ejecute en la VM, sea el script de instalación de un paquete, un stealer o una inyección de prompt, encuentra un sustituto ghp_ en ~/.git-credentials y GH_TOKEN. Un atacante que lo reutilice desde su propia máquina recibe un 401 de GitHub.

El stealer además tiene que enviar lo que se llevó a algún sitio, y esa petición atraviesa el proxy. Un sustituto tiene un único destino legítimo, el host para el que se generó. El proxy examina cada petición saliente, cabeceras y cuerpo, en busca de cualquier sustituto que vaya a otra parte. Un token ghp_ camino del servidor de un stealer recibe un HTTP 451, no se reenvía nada, la VM se pausa, y en el Mac se abre This environment may have been compromised con una vista previa del token, el nombre de la credencial, el host al que pertenece y el host al que se envió.

Un push de workflow espera un clic

Los gusanos de esta familia no siempre esperan a un comprador. La carga útil de tensorlake la semana pasada hacía commit de archivos en los repositorios de sus víctimas directamente desde la máquina del desarrollador. Las máquinas Bromure nuevas usan Prompt before write para cada forja git. Un push es una escritura, así que hacer commit de security-audit.yml en un repositorio desde la VM se detiene en un diálogo de macOS, Allow write on "github.com" from workspace "…"?, que muestra POST /owner/repo.git/git-receive-pack tal como pasó por el cable. El barrido de henrywoo hizo 318 de esas escrituras en 16 minutos. Desde una máquina Bromure, la primera te espera, y tú no le pediste a tu agente que añadiera una auditoría de seguridad a 318 repositorios.

Cada uso del token queda registrado

La Security Timeline registra cada sustitución de credencial como una fila Credential brokering: qué token, qué host, qué máquina, a qué hora. Cuando necesites saber si tu token de GitHub hizo push de algo el 8 de octubre entre las 21:10 y las 21:26, la respuesta está en tu Mac, en un registro que el agente no puede editar. Con el trazado activado, bromure-cli trace leaks lista cada petición en la que un token apareció en el host equivocado.

En Bromure: sustitutos en el historial, claves reales en el MacVM de la máquina (Ubuntu)GH_TOKEN=ghp_… (sustituto)ANTHROPIC_API_KEY=sk-ant-api03-brm-…El commit del agente+ KEY="sk-ant-api03-brm-…"un stealer envía el ghp_ fueragit push security-audit.ymlningún secreto real que robarProxy, en el MacValor real puesto solo parael host de la credencialSustituto a otro host → 451,VM en pausa, alerta en el Macgit-receive-pack → Prompt before writeSecurity Timeline: cada cambio,token, host, máquina, horaHistorial de GitHubsk-ant-api03-brm-…El grep de GhostActionencuentra sk-ant- ylo envíaAnthropic: 401GitHub: 401el comprador recibecadenas inserviblesGuardado en el Mac, fuera de la VMClaves de Anthropic, OpenAI, OpenRouter · token de GitHub · AWS · claves SSH · trazas · Security Timeline
La misma cadena contra una máquina Bromure. El historial que lee el workflow contiene sustitutos, el token que un stealer encuentra en la VM es un sustituto, y cualquier petición que lleve uno al host equivocado se detiene en el Mac.

Limpiar después de una clave

El consejo habitual tras filtrar una clave es rotarla, reescribir el historial con git filter-repo, hacer force-push y esperar que ningún fork ni espejo haya copiado antes los commits viejos. GhostAction muestra por qué esa última parte falla. Socket contó 279 forks en un solo espacio de nombres, cada uno con el historial y, ahora, con el workflow. Una clave que llegó a un commit público está perdida, muestre lo que muestre el árbol actual.

Una máquina Bromure cambia lo que llega al commit en primer lugar. El agente puede ser tan descuidado con las claves como dicen las cifras de GitGuardian que son los agentes. Puede pegar una clave en un fixture, hacer commit de un .env y olvidarse. Nunca tuvo una clave que funcionara, así que nada de ese historial puede hacerte daño.

El commit contiene un sustituto

Los agentes de la VM solo ven sustitutos con la forma de las claves reales. Una clave escrita en un archivo y con commit es una cadena -brm- que solo funciona a través del proxy de tu Mac.

La cosecha recibe 401

Los patrones de GhostAction encuentran los sustitutos y los envían. Anthropic, OpenAI y GitHub los rechazan en cualquier sitio fuera de tu Mac.

Un token robado detiene la máquina

El token de GitHub de la VM también es un sustituto. Enviado a cualquier sitio que no sea GitHub, recibe un 451, la VM se pausa y una alerta nombra el token y el host.

Los push esperan un clic

Prompt before write retiene cada push desde la VM en un diálogo de macOS que muestra la petición, y la Security Timeline registra cada uso del token.

El próximo grep

Es la tercera ola de GhostAction en trece meses. La primera, en septiembre de 2025, se llevó 3.325 secretos de CI. La segunda, del 31 de agosto al 30 de septiembre de este año, fue a por 2.577 secretos en 772 repositorios. Esta ha añadido el historial completo y tres patrones de proveedores de IA. La próxima ola añadirá más patrones, y el historial que lee no hace más que crecer.

Ejecuta tus agentes donde las claves que tocan sean sustitutos, y el historial que dejan atrás no guardará nada que un comprador pueda usar. Para saber más sobre cómo funcionan los sustitutos y qué pasa cuando uno se filtra, lee El agente filtró su propia clave y La clave nunca caducó.