← Retour à tous les articles
Publié le · par Renaud Deraison

La clé supprimée était encore dans l'historique

Le 8 octobre, une campagne baptisée GhostAction a utilisé deux comptes de mainteneurs piratés pour committer un workflow nommé security-audit.yml dans 346 dépôts, dont athenadriver d'Uber et pyxel et ses 18 000 étoiles. Le 9 octobre, Socket comptait plus de 500 comptes et des dizaines de milliers de dépôts. Le workflow lance git log -p --all et passe au grep chaque commit jamais fait à la recherche de clés Anthropic, OpenAI, OpenRouter, GitHub et AWS, y compris celles supprimées un commit plus tard. Dans Bromure Agentic Coding, l'agent ne détient jamais que des substituts, si bien que les clés qu'il écrit dans l'historique d'un dépôt ne valent rien pour qui les trouve.

Supprimer une clé d'un fichier prend un commit. Le commit précédent garde la clé aussi longtemps que le dépôt existe.

Le jeudi 8 octobre à 13 h 40 UTC, le compte GitHub de Takashi Kitao, auteur du moteur de jeu pyxel, a commencé à committer un fichier nommé .github/workflows/security-audit.yml. Quatre minutes plus tard, il se trouvait dans 27 de ses dépôts. À 21 h 10 UTC, le compte henrywoo, auteur d'origine d'athenadriver chez Uber, a lancé le même balayage. À 21 h 26, il avait atteint 318 dépôts, dont 279 forks et uber/athenadriver lui-même, où le compte a un accès en écriture.

Socket a publié son analyse le 9 octobre et a attribué ces commits à GhostAction, une campagne de vol d'identifiants apparue pour la première fois en septembre 2025. À la fin de cette journée, la mise à jour de Socket comptait plus de 500 comptes GitHub ayant committé le workflow dans « des dizaines de milliers de dépôts » depuis le 7 octobre. The Hacker News et StepSecurity ont couvert la même vague. Socket a constaté que le workflow s'était déjà exécuté dans des dépôts touchés, et qu'il se trouvait encore sur les branches par défaut de kitao/pyxel et uber/athenadriver au moment de la publication.

Ce que fait security-audit.yml

Le workflow s'appelle « Security Audit ». Il se déclenche sur workflow_dispatch et à chaque push sur n'importe quelle branche ou tag, sans aucun filtre. Son étape de checkout utilise fetch-depth: 0, qui récupère toutes les branches, tous les tags et l'historique complet. Ensuite, une seule étape fait deux choses.

La première s'en prend aux secrets GitHub Actions du dépôt. Avant de committer le workflow, l'attaquant a lu les workflows existants du dépôt et a écrit dans la charge utile les noms des secrets qu'ils utilisent. Pour pyxel, il s'agissait de CARGO_REGISTRY_TOKEN, PERSONAL_ACCESS_TOKEN, PYPI_PASSWORD et PYPI_USERNAME.

La seconde est nouvelle dans cette vague. L'étape passe l'arborescence au grep avec 13 motifs d'identifiants, puis lance git log -p --all et cherche les mêmes motifs dans jusqu'à 200 000 lignes d'historique :

  • Services d'IA : Anthropic (sk-ant-), clés de projet OpenAI (sk-proj-), OpenRouter (sk-or-)
  • Gestion de code source : tokens GitHub classiques (ghp_), tokens à granularité fine (github_pat_), tokens GitLab (glpat-)
  • AWS : identifiants de clé d'accès, identifiants de clé temporaire, clés secrètes et tokens de session. L'étape garde aussi deux lignes de contexte autour de chaque identifiant de clé, si bien que la clé secrète committée à côté part avec.
  • SaaS et cloud : clés d'API Google, tokens Slack, clés SendGrid

Tout part dans un seul POST curl en HTTP non chiffré vers 193.32.204.199. Chaque exécution transmet le nom du dépôt, qu'elle ait trouvé quelque chose ou non.

Socket pointe le public visé. Deux des dépôts de henrywoo sont pyllama et chatllama, des projets d'apprentissage automatique dont les contributeurs et les auteurs de forks « détiennent probablement ce type de clés ». Une clé Anthropic ou OpenAI se revend comme de l'inférence facturable, et chaque prompt envoyé avec elle passe entre les mains de l'acheteur.

L'historique garde tout

Les conseils de remédiation de Socket contiennent une phrase qui devrait inquiéter quiconque a déjà nettoyé une clé fuitée en modifiant un fichier : « Faites tourner chaque identifiant trouvé dans n'importe quel commit, qu'il soit encore ou non dans l'arborescence actuelle. » Une clé retirée au commit suivant est toujours dans le commit d'avant. git log -p --all lit les deux.

Les agents de code alimentent vite cet historique. Le rapport State of Secrets Sprawl 2026 de GitGuardian a constaté que les commits publics co-écrits par Claude Code fuitaient des secrets dans 3,2 % des cas, contre une base de 1,5 % sur l'ensemble de GitHub public. Le même rapport a compté 1 275 105 secrets fuités liés à des services d'IA en 2025, 81 % de plus que l'année précédente.

Le scénario est familier à quiconque a regardé un agent faire passer un test d'intégration qui échouait. L'agent a la clé dans son environnement. Il l'écrit dans un fichier de config, une fixture de test ou un .env qui n'est pas encore ignoré, parce que ça fait passer le test. Il committe. Plus tard, le développeur ou l'agent déplace la clé dans une variable d'environnement et committe à nouveau. L'arborescence a l'air propre. L'historique contient la clé, chaque fork a l'historique, et un workflow avec fetch-depth: 0 le lit.

Le token qui a écrit le workflow

Socket n'a pas déterminé comment les deux comptes ont été pris. The Hacker News désigne la source la plus probable : un personal access token issu de journaux d'infostealers ou de dumps d'identifiants. Les journaux d'infostealers viennent de machines de développeurs. Sur un portable, le token GitHub se trouve dans ~/.config/gh/hosts.yml, dans ~/.git-credentials et souvent dans GH_TOKEN, lisible par tout ce qui tourne sous l'identité du développeur, y compris ce qu'un agent de code installe ou exécute.

Un seul token suffit. Avec lui, l'attaquant peut lister les dépôts où le compte a le droit d'écrire, lire leurs workflows pour y trouver les noms de secrets, et committer dans chacun. Le balayage de henrywoo a couvert 318 dépôts en 16 minutes.

Un token volé, un workflow, chaque commitPortable du mainteneur~/.config/gh/hosts.yml~/.git-credentialsGH_TOKEN=ghp_… (réel)log d'infostealer→ token à vendresource probable (THN)security-audit.yml : 318 dépôts, 16 minon: push (any branch), workflow_dispatchfetch-depth: 0git log -p --all | grep 13 patternsHistorique de config/test.pya1f3 + KEY="sk-ant-api03-…" ← trouvéec92e - KEY="sk-ant-api03-…" + KEY=os.environ["…"]Arbre propre, clé encore dans l'historique.193.32.204.199POST (HTTP en clair)REPO=…PYPI_PASSWORD=…sk-ant-api03-…AKIA_CTX_START…clés actuelles etsupprimées, toutChaque fork porte le même historique. Socket a compté 279 forks rien que chez henrywoo.
La chaîne GhostAction. Un token venu d'une machine de développeur committe le workflow, le workflow lit chaque commit jamais fait, et chaque clé de cet historique, actuelle ou supprimée, part dans un seul POST.

Le même dépôt, écrit depuis une machine Bromure

Dans Bromure Agentic Coding, l'agent tourne dans une VM Linux sur votre Mac, et chaque requête HTTP de la VM traverse un proxy sur l'hôte. Votre clé Anthropic, votre token GitHub et vos clés AWS restent chiffrés sur le Mac. Lâchez maintenant GhostAction sur un dépôt écrit par une machine Bromure, et sur le développeur qui l'a écrit.

L'historique contient des substituts

Dans la VM, chaque identifiant est un substitut qui a la forme du vrai : ANTHROPIC_API_KEY vaut sk-ant-api03-brm-…, une clé OpenRouter vaut sk-or-v1-brm-…, GH_TOKEN et ~/.git-credentials contiennent ghp_ suivi de 36 caractères. Chacun est dérivé de la vraie valeur et d'un sel propre à l'installation avec HKDF-SHA256, et ne peut pas être reconverti en elle. Le proxy substitue la vraie valeur sur le réseau, et seulement pour une requête vers l'hôte auquel l'identifiant appartient.

Donc quand l'agent écrit sa clé dans config/test.py pour faire passer le test, il écrit ce qu'il a. Quand il committe, le commit contient sk-ant-api03-brm-…. Quand le commit de nettoyage arrive plus tard, l'historique garde ce même substitut.

Le grep de GhostAction le trouve. sk-ant- correspond à sk-ant-api03-brm-, et ghp_ correspond à un token GitHub de substitution. Le POST vers 193.32.204.199 les emporte, et l'acheteur les essaie. Anthropic rejette la clé et GitHub rejette le token, parce que la seule chose qui en fait des identifiants valides est le proxy sur votre Mac, pour les requêtes qui sortent de votre VM. Le conseil de Socket de faire tourner chaque clé trouvée dans n'importe quel commit a une réponse courte pour ce dépôt : aucune de ces clés n'a jamais fonctionné hors de votre Mac, et les substituts des fournisseurs d'IA portent -brm- dans leur nom, ce qui permet de les repérer lors d'un scan de l'historique.

Le token du portable est aussi un substitut

L'autre moitié de la campagne, c'est le compte lui-même. Faites tourner vos agents dans une machine Bromure, et le token GitHub qu'ils utilisent reste sur le Mac. Tout ce qui s'exécute dans la VM, qu'il s'agisse du script d'installation d'un paquet, d'un stealer ou d'une injection de prompt, trouve un substitut ghp_ dans ~/.git-credentials et GH_TOKEN. Un attaquant qui le rejoue depuis sa propre machine reçoit un 401 de GitHub.

Le stealer doit aussi envoyer son butin quelque part, et cette requête traverse le proxy. Un substitut n'a qu'une destination légitime, l'hôte pour lequel il a été créé. Le proxy inspecte chaque requête sortante, en-têtes et corps, à la recherche de tout substitut en route vers un autre hôte. Un token ghp_ qui part vers le serveur d'un stealer reçoit un HTTP 451, rien n'est transmis, la VM se met en pause, et This environment may have been compromised s'ouvre sur le Mac avec un aperçu du token, le nom de l'identifiant, l'hôte auquel il appartient et l'hôte vers lequel il a été envoyé.

Un push de workflow attend un clic

Les vers de cette famille n'attendent pas toujours un acheteur. La charge utile de tensorlake la semaine dernière committait des fichiers dans les dépôts de ses victimes directement depuis la machine du développeur. Les nouvelles machines Bromure utilisent Prompt before write pour chaque forge git. Un push est une écriture : committer security-audit.yml dans un dépôt depuis la VM s'arrête donc sur une boîte de dialogue macOS, Allow write on "github.com" from workspace "…"?, qui montre POST /owner/repo.git/git-receive-pack tel qu'il est passé sur le réseau. Le balayage de henrywoo a fait 318 de ces écritures en 16 minutes. Depuis une machine Bromure, la première vous attend, et vous n'avez pas demandé à votre agent d'ajouter un audit de sécurité à 318 dépôts.

Chaque usage du token est enregistré

La Security Timeline enregistre chaque substitution d'identifiant dans une ligne Credential brokering : quel token, quel hôte, quelle machine, à quelle heure. Quand vous devez savoir si votre token GitHub a poussé quoi que ce soit le 8 octobre entre 21 h 10 et 21 h 26, la réponse est sur votre Mac, dans un registre que l'agent ne peut pas modifier. Avec le traçage activé, bromure-cli trace leaks liste chaque requête où un token est apparu sur le mauvais hôte.

Dans Bromure : substituts dans l'historique, vraies clés restées sur le MacVM de la machine (Ubuntu)GH_TOKEN=ghp_… (substitut)ANTHROPIC_API_KEY=sk-ant-api03-brm-…Le commit de l'agent+ KEY="sk-ant-api03-brm-…"un stealer exfiltre le ghp_git push security-audit.ymlaucun vrai secret à volerProxy, sur le MacVraie valeur insérée seulementpour l'hôte de l'identifiantSubstitut vers autre hôte → 451,VM en pause, alerte sur le Macgit-receive-pack → Prompt before writeSecurity Timeline : chaquetoken, hôte, machine, heureHistorique GitHubsk-ant-api03-brm-…Le grep GhostActiontrouve sk-ant- etl'envoieAnthropic : 401GitHub : 401l'acheteur obtient deschaînes sans valeurGardé sur le Mac, hors de la VMClés Anthropic, OpenAI et OpenRouter · token GitHub · clés AWS · clés privées SSH · traces · Security Timeline
La même chaîne face à une machine Bromure. L'historique que lit le workflow contient des substituts, le token qu'un stealer trouve dans la VM est un substitut, et toute requête qui en emporte un vers le mauvais hôte s'arrête sur le Mac.

Faire le ménage après une clé

Le conseil habituel après une fuite de clé est de la faire tourner, de réécrire l'historique avec git filter-repo, de forcer le push, et d'espérer qu'aucun fork ni miroir n'a copié les anciens commits avant. GhostAction montre pourquoi cette dernière partie échoue. Socket a compté 279 forks dans un seul espace de noms, chacun portant l'historique et, désormais, le workflow. Une clé qui a atteint un commit public est perdue, quoi que montre l'arborescence actuelle.

Une machine Bromure change ce qui atteint le commit en premier lieu. L'agent peut être aussi négligent avec les clés que les chiffres de GitGuardian le disent des agents. Il peut coller une clé dans une fixture, committer un .env et l'oublier. Il n'a jamais détenu de clé valide, donc rien dans cet historique ne peut vous nuire.

Le commit contient un substitut

Les agents dans la VM ne voient que des substituts qui ont la forme des vraies clés. Une clé écrite dans un fichier et committée est une chaîne -brm- qui ne fonctionne qu'à travers le proxy sur votre Mac.

La récolte obtient des 401

Les motifs de GhostAction trouvent les substituts et les expédient. Anthropic, OpenAI et GitHub les rejettent partout hors de votre Mac.

Un token volé arrête la machine

Le token GitHub dans la VM est lui aussi un substitut. Envoyé ailleurs que vers GitHub, il reçoit un 451, la VM se met en pause, et une alerte nomme le token et l'hôte.

Les pushes attendent un clic

Prompt before write retient chaque push depuis la VM sur une boîte de dialogue macOS qui montre la requête, et la Security Timeline enregistre chaque usage du token.

Le prochain grep

C'est la troisième vague de GhostAction en treize mois. La première, en septembre 2025, a pris 3 325 secrets de CI. La deuxième, du 31 août au 30 septembre de cette année, a visé 2 577 secrets dans 772 dépôts. Celle-ci a ajouté l'historique complet et trois motifs de fournisseurs d'IA. La prochaine vague ajoutera d'autres motifs, et l'historique qu'elle lit ne fait que grandir.

Faites tourner vos agents là où les clés qu'ils manipulent sont des substituts, et l'historique qu'ils laissent derrière eux ne contient rien qu'un acheteur puisse utiliser. Pour en savoir plus sur le fonctionnement des substituts et ce qui se passe quand l'un d'eux fuit, lisez L'agent a divulgué sa propre clé et La clé n'a jamais expiré.