
Fixtures GitHub Actions reproductibles, vulnérables et corrigées, pour l'injection de workflows agentiques (CVE-2026-44246), avec couverture de détection mesurée et recommandations d'atténuation.
Fixtures reproductibles vulnérables/corrigées pour la classe de bug publiée sous CVE-2026-44246 (nnU-Net, injection de workflow agentique, CVSS 7.2, CWE-1427), ainsi que la sortie mesurée de trois détecteurs face à celles-ci.
Il existe parce qu'il n'y avait rien pour tester un détecteur. Écrire une règle pour cette classe signifie écrire une fixture à la main et espérer qu'elle soit fidèle ; les révisions publiées n'étaient disponibles qu'en sachant quel commit regarder, ce qui s'est avéré être la partie intéressante.
Un workflow GitHub Actions confie du texte non fiable à un agent IA qui possède les identifiants du dépôt.
Quatre conditions, toutes nécessaires :
issues, issue_comment, pull_request_review —
des événements dont le texte peut être rédigé par quiconque possède un compte GitHub.issues: write, contents: write, etc.claude-code-action et
codex-action refusent un acteur d'exécution sans permission d'écriture sauf si une entrée
(allowed_non_write_users, allow-users) l'autorise explicitement. Sans
cette entrée, un auteur non fiable n'atteint jamais l'agent, et le workflow est
la configuration sûre plutôt que celle-ci.${{ github.event.issue.body }} à l'intérieur de prompt:) soit récupéré à l'exécution
(gh issue view) avec le token que le job fournit.Ce n'est pas de l'injection de template. Rien n'est évalué comme du shell ; la charge utile est de la prose,
et l'interpréteur est le modèle. C'est pourquoi le conseil habituel — citez
vos variables, ne faites pas d'eval — ne s'applique pas, et pourquoi le correctif ci-dessous concerne
ce que l'agent peut atteindre plutôt que l'échappement de l'entrée.
nnU-Net a durci ce workflow en deux commits, et un détecteur qui traite « avant » et « après » comme un binaire se trompe sur celui du milieu.
| révision | commit | date | --allowedTools de l'agent | verdict |
|---|---|---|---|---|
| vulnérable | 94300b49e716 | 2026-04-13 | gh issue comment, gh issue edit | écriture atteignable |
| « le correctif » | 4e4770b0b0e6 | 2026-04-24 | gh issue comment uniquement ; l'étiquetage déplacé vers un script wrapper | écriture toujours atteignable |
| ultérieure | 11bd8746fc06 | 2026-04-27 | aucune ; une étape ultérieure publie depuis un fichier que l'agent écrit | non atteignable |
Le commit que tout le monde appellerait le correctif — son message est « hardened issue
and PR agents » — a supprimé gh issue edit et routé les étiquettes via
.github/scripts/safe-label.sh, mais a laissé Bash(gh issue comment:*) avec
l'agent. L'agent pouvait toujours être orienté vers un commentaire, et le motif
d'allowlist gh issue comment:* n'est pas limité à l'issue déclencheuse, donc la
cible était au choix du modèle. Seul le troisième commit l'a fermé : l'agent écrit désormais
/tmp/issue-comment.md et une étape ultérieure, non-agent, le publie avec
ISSUE: ${{ github.event.issue.number }} pris depuis l'événement.
Deux choses en découlent, et c'est pourquoi ce dépôt n'est pas juste deux fichiers :
« Corrigé » est une affirmation sur une révision, pas sur une version. v2.4.1 est cité comme la
version corrigée, mais son .github/workflows/ ne contient que codespell.yml — les
workflows d'agent ne sont pas du tout dans ce tag. Vous ne pouvez pas vérifier le correctif depuis le
tag ; vous devez épingler le commit.
Le bloc de permissions ne change jamais. issues: write est présent et correct
dans les trois révisions, y compris la dernière, car une étape ultérieure en a besoin. Un
détecteur qui se base uniquement sur permissions ne peut pas séparer la révision 1 de
la révision 3. Ce qui les sépare, c'est ce que l'agent peut appeler.
Trois détecteurs, exécutés sur les deux fixtures et les trois révisions réelles. Sortie brute complète et versions des outils dans results.md.
| révision | agentbound 0.1.3 | sisakulint v0.3.7 | zizmor 1.30.1 |
|---|---|---|---|
1-vulnerable | HIGH write-scope, HIGH untrusted-content | ai-action-prompt-injection | — |
2-fix-commit | HIGH write-scope, CRITICAL author-association | — (générique uniquement) | — |
3-later | LOW write-scope, CRITICAL author-association | ai-action-excessive-tools, ai-action-execution-order | — |
Aucun détecteur n'est simplement « faux » ici — ils répondent à des questions différentes :
ai-action-prompt-injection se déclenche sur la révision vulnérable,
où le corps de l'issue est interpolé dans prompt:, et s'arrête une fois
l'interpolation disparue. Correct. Ses constats sur la révision ultérieure méritent
d'être lus avant d'agir dessus — ai-action-excessive-tools signale
Write, que ce workflow utilise volontairement pour écrire le fichier qu'une étape
ultérieure publie ; et ai-action-execution-order veut l'agent en dernier, ce qui est
exactement ce que cette conception évite, puisque les étapes privilégiées viennent après
la fin du tour du modèle.permissions du job. Son
constat ci-agent-missing-author-association est rapporté à critical sur la
révision ultérieure et son propre README décrit cela comme trop sévère plutôt que
faux : auto-triage est censé être atteignable par n'importe qui.Chaque outil ici a un constat qu'il rapporte sur une révision dont l'auteur avait déjà raisonné sur cette condition exacte. C'est l'état normal d'une heuristique, et la raison pour laquelle une table de couverture est plus utile qu'une colonne réussite/échec.
git clone https://github.com/sushant-me/agentic-workflow-injection
cd agentic-workflow-injection
bash scripts/fetch-revisions.sh # the three real revisions, pinned by SHA
bash scripts/benchmark.sh # runs every detector that is installed
benchmark.sh signale un détecteur manquant comme manquant plutôt que de
le sauter silencieusement, car une table de couverture qui omet discrètement un outil ne prouve
rien à son sujet.
Les fixtures sont la forme réduite, écrites pour ce dépôt et sous licence MIT
comme le reste. Elles sont fidèles au mécanisme plutôt que des copies :
fixtures/vulnerable.yml porte les quatre conditions et rien d'autre, et
fixtures/fixed.yml est le même fichier avec les six changements qui comptent, chacun
annoté. Si vous ajoutez une règle, ces deux fichiers sont les plus petites entrées qu'elle
doit traiter correctement.
Deux particularités d'interface, gérées dans le script mais bonnes à connaître :
not found et sort avec le code 3. Sans surveillance, cela ressemble exactement à un scan
propre.path JSON d'agentbound est un basename, donc un scan de répertoire sur des fichiers portant
le même nom est ambigu.La détection est la moitié facile. docs/mitigations.md couvre l'autre moitié : comment borner l'autorité d'un agent, avec les couches classées selon une question — ce contrôle dépend-il du fait que le modèle choisisse de se conformer ?
Ce classement est tout l'intérêt. Une instruction dans un prompt est une entrée, pas une clause de garde ; un motif d'outil qui nomme sa cible, ou un token que l'agent ne détient jamais, est une borne. Le guide descend de « le modèle ne peut pas faire la mauvaise chose » à « on a demandé au modèle de ne pas le faire », avec une checklist et l'évolution de nnU-Net comme exemple concret.
fixtures/fixed.yml étiquette chacun de ses six changements avec la couche qu'il implémente
et avec le fait qu'il soit porteur ou non — deux d'entre eux ne le sont pas, et sont étiquetés
ainsi pour qu'un lecteur ne surestime pas le fichier.
Récupéré à l'exécution par SHA de commit, jamais vendu :
| fichier | dépôt | commit | chemin |
|---|---|---|---|
real/1-vulnerable.yml | MIC-DKFZ/nnUNet | 94300b49e716 | .github/workflows/issue-triage.yml |
real/2-fix-commit.yml | MIC-DKFZ/nnUNet | 4e4770b0b0e6 | .github/workflows/issue-agent.yml |
real/3-later.yml | MIC-DKFZ/nnUNet | 11bd8746fc06 | .github/workflows/issue-agent.yml |
Aucune copie de la configuration de nnU-Net n'est redistribuée ici ; relancez la récupération et comparez vous-même les octets avec les blobs ci-dessus.
Ceci est un artefact d'ingénierie de détection. Il ne contient aucun exploit, et rien
ici n'a été testé contre un workflow en production — les révisions vulnérables sont des fichiers
statiques, déjà publics dans l'historique de nnU-Net et référencés par un avis
publié. Les fixtures sont marquées do not deploy parce qu'elles existent pour être
détectées, pas copiées.
Si vous maintenez un détecteur pour cette classe et voulez une ligne dans la table, le
script de benchmark est l'interface : ajoutez une section qui exécute votre outil sur
targets et affiche ses constats, et les fixtures et révisions épinglées font le
reste.
MIT.