
Fixture GitHub Actions riproducibili, vulnerabili e corrette, per l'iniezione di workflow agentici (CVE-2026-44246), con copertura del rilevatore misurata e indicazioni di mitigazione.
Fixture riproducibili vulnerabili/corrette per la classe di bug pubblicata come CVE-2026-44246 (nnU-Net, iniezione di workflow agentico, CVSS 7.2, CWE-1427), più l'output misurato di tre rilevatori su di esse.
Esiste perché non c'era nulla con cui testare un rilevatore. Scrivere una regola per questa classe significa scrivere una fixture a mano e sperare che sia fedele; le revisioni pubblicate erano disponibili solo sapendo quale commit guardare, il che si è rivelato la parte interessante.
Un workflow di GitHub Actions consegna testo non attendibile a un agente AI che possiede le credenziali del repository.
Quattro condizioni, tutte necessarie:
issues, issue_comment, pull_request_review —
eventi il cui testo chiunque abbia un account GitHub può scrivere.issues: write, contents: write, e così via.claude-code-action e
codex-action rifiutano un attore di esecuzione senza permesso di scrittura a meno che un input
(allowed_non_write_users, allow-users) non lo abiliti esplicitamente. Senza
quell'input un autore non attendibile non raggiunge mai l'agente, e il workflow è
la configurazione sicura anziché questa.${{ github.event.issue.body }} dentro prompt:) sia recuperato a runtime
(gh issue view) con il token che il job consegna.Non è template injection. Nulla viene valutato come shell; il payload è prosa,
e l'interprete è il modello. Ecco perché il consiglio abituale — metti tra
virgolette le tue variabili, non usare eval — non si applica, e perché la correzione
sotto riguarda ciò che l'agente può raggiungere piuttosto che l'escaping dell'input.
nnU-Net ha rafforzato questo workflow in due commit, e un rilevatore che tratta "prima" e "dopo" come binari sbaglia quello intermedio.
| revisione | commit | data | --allowedTools dell'agente | verdetto |
|---|---|---|---|---|
| vulnerabile | 94300b49e716 | 2026-04-13 | gh issue comment, gh issue edit | scrittura raggiungibile |
| "la correzione" | 4e4770b0b0e6 | 2026-04-24 | solo gh issue comment; l'etichettatura spostata in uno script wrapper | scrittura ancora raggiungibile |
| successiva | 11bd8746fc06 | 2026-04-27 | nessuno dei due; uno step successivo pubblica da un file che l'agente scrive | non raggiungibile |
Il commit che tutti chiamerebbero la correzione — il suo messaggio è "hardened issue
and PR agents" — ha rimosso gh issue edit e instradato le etichette attraverso
.github/scripts/safe-label.sh, ma ha lasciato Bash(gh issue comment:*) con
l'agente. L'agente poteva ancora essere indirizzato a commentare, e il pattern
dell'allowlist gh issue comment:* non è limitato all'issue che ha attivato il
workflow, quindi l'obiettivo era a scelta del modello. Solo il terzo commit l'ha chiuso: l'agente ora
scrive /tmp/issue-comment.md e uno step successivo, non-agentico, lo pubblica con
ISSUE: ${{ github.event.issue.number }} preso dall'evento.
Ne seguono due cose, ed è per questo che questo repository non è solo due file:
"Corretto" è un'affermazione su una revisione, non su una versione. v2.4.1 è citato come
la release corretta, ma il suo .github/workflows/ contiene solo codespell.yml — i
workflow dell'agente non sono affatto in quel tag. Non puoi verificare la correzione dal
tag; devi fissare il commit.
Il blocco dei permessi non cambia mai. issues: write è presente e corretto
in tutte e tre le revisioni, inclusa l'ultima, perché uno step successivo ne ha bisogno. Un
rilevatore che si basa solo su permissions non può separare la revisione 1 dalla
revisione 3. Ciò che le separa è cosa l'agente può chiamare.
Tre rilevatori, eseguiti su entrambe le fixture e su tutte e tre le revisioni reali. Output grezzo completo e versioni degli strumenti in results.md.
| revisione | 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 | — (solo generico) | — |
3-later | LOW write-scope, CRITICAL author-association | ai-action-excessive-tools, ai-action-execution-order | — |
Nessun rilevatore è semplicemente "sbagliato" qui — rispondono a domande diverse:
ai-action-prompt-injection scatta sulla revisione
vulnerabile, dove il corpo dell'issue è interpolato in prompt:, e si ferma una volta
che l'interpolazione è sparita. Corretto. I suoi risultati sulla revisione successiva sono
da leggere prima di agire su di essi — ai-action-excessive-tools segnala
Write, che questo workflow usa di proposito per scrivere il file che uno step
successivo pubblica; e ai-action-execution-order vuole l'agente per ultimo, che è
esattamente ciò che questo design evita, dato che gli step privilegiati vengono dopo
che il turno del modello è terminato.permissions del job. Il suo
risultato ci-agent-missing-author-association è riportato come critical sulla
revisione successiva e il suo stesso README lo descrive come eccessivamente severo anziché
falso: auto-triage è pensato per essere raggiungibile da chiunque.Ogni strumento qui ha un risultato che riporta su una revisione il cui autore aveva già ragionato su quella esatta condizione. Questo è lo stato normale di un'euristica, e il motivo per cui una tabella di copertura è più utile di una colonna pass/fail.
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 segnala un rilevatore mancante come mancante anziché
saltarlo silenziosamente, perché una tabella di copertura che omette tranquillamente uno strumento non
prova nulla al riguardo.
Le fixture sono la forma ridotta, scritte per questo repository e con licenza MIT
come il resto. Sono fedeli al meccanismo anziché copie:
fixtures/vulnerable.yml porta le quattro condizioni e nulla più, e
fixtures/fixed.yml è lo stesso file con le sei modifiche che contano, ciascuna
annotata. Se stai aggiungendo una regola, quei due file sono gli input più piccoli che
deve gestire correttamente.
Due stranezze dell'interfaccia, gestite nello script ma che vale la pena conoscere:
not found ed esce con 3. Non osservato, sembra identico a una scansione pulita.path JSON di agentbound è un basename, quindi una scansione di directory su file con
lo stesso nome è ambigua.Il rilevamento è la metà facile. docs/mitigations.md copre l'altra metà: come limitare l'autorità di un agente, con i livelli classificati secondo una domanda — questo controllo dipende dal fatto che il modello scelga di conformarsi?
Quella classificazione è il punto centrale. Un'istruzione in un prompt è input, non una guard clause; un pattern di strumento che nomina il suo obiettivo, o un token che l'agente non possiede mai, è un limite. La guida scende da "il modello non può fare la cosa sbagliata" a "al modello è stato chiesto di non farlo", con una checklist e l'evoluzione di nnU-Net come esempio pratico.
fixtures/fixed.yml etichetta ciascuna delle sue sei modifiche con il livello che implementa
e con se è portante — due di esse non lo sono, e sono etichettate
così affinché un lettore non sovrastimi il file.
Recuperate a runtime tramite SHA del commit, mai incluse nel repository:
| file | repository | commit | path |
|---|---|---|---|
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 |
Nessuna copia della configurazione di nnU-Net è ridistribuita qui; riesegui il fetch e confronta tu stesso i byte con i blob sopra.
Questo è un artefatto di detection-engineering. Non contiene alcun exploit, e nulla
qui è stato testato contro un workflow live — le revisioni vulnerabili sono file
statici, già pubblici nella storia di nnU-Net e referenziati da un advisory
pubblicato. Le fixture sono marcate do not deploy perché esistono per essere
rilevate, non copiate.
Se mantieni un rilevatore per questa classe e vuoi una riga nella tabella, lo
script di benchmark è l'interfaccia: aggiungi una sezione che esegue il tuo strumento su
targets e stampa i suoi risultati, e le fixture e le revisioni fissate fanno il
resto.
MIT.