Conversation
Signed-off-by: Shenao Wang <shenaowang@foxmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Attacker-controlled GitHub event data (
github.event.issue.title) flows into an AI agent prompt in.github/workflows/gemini-issue-triage.ymlwithout sanitization or access control, allowing any user who can trigger the workflow to inject arbitrary instructions into the agent.Solution
This GitHub Actions workflow is vulnerable to Agentic Workflow Injection:
attacker-controlled GitHub event data is interpolated directly into an AI
agent prompt. Any user who can trigger the workflow (by opening an issue,
commenting, opening a PR, or creating a branch) can inject arbitrary
instructions into the agent.
Apply BOTH of the following fixes. Each is necessary; together they provide
defense in depth.
Fix 1 — Add an actor gate at the JOB level (PRIMARY defense)
Add an
if:condition immediately under the job name, beforeruns-on:If an
if:condition already exists on that job, combine them with&&rather than replacing (so existing logic is preserved), e.g.:
This is the root-cause fix: untrusted users cannot reach the agent at all.
Fix 2 — Replace direct interpolation with ID-based agent fetch
For every occurrence of an attacker-controlled expression in a
prompt:,instructions:, orrun:field, replace it with the event's numeric IDand add instructions for the agent to fetch the content via
ghCLI.Replacement mapping (use whichever matches the original expression):
Example rewrite — BEFORE:
AFTER (the Title/Body lines are REMOVED, not kept alongside the new
instructions):
CRITICAL: every original line that interpolated the tainted expression
(e.g.
Title: ${{ github.event.issue.title }},Body: ${{ ... }},Título: ${{ pull_request.title }}, etc.) MUST BE DELETED from theprompt. It is NOT enough to ADD the
gh ... viewinstructions —leaving the original lines in place means the attacker-controlled data
is STILL interpolated into the prompt and the vulnerability is NOT fixed.
After your patch, grep the resulting prompt for every
${{ github.event.*. (title|body|label|ref) }}style expression that isn't.numberor.id—there should be ZERO matches inside any
prompt:,instructions:,run:,or equivalent string field.
Rationale: this moves attacker-controlled bytes out of the agent's
system prompt (where they are interpreted as high-trust instructions)
into tool output (which modern agents are trained to treat as
untrusted data). Combined with Fix 1, this yields strong defense.
Fix 3 — Ensure the agent CAN actually run
gh(prerequisites for Fix 2)For the ID-based fetch in Fix 2 to work at runtime, the agent must have:
(a) Read permission on the event source. Inspect the workflow's
permissions:block (job-level or top-level). Add the missing scopeif not already present:
- For issue/comment taint sources → needs
issues: read- For PR/review/branch taint sources → needs
pull-requests: read- For discussion taint sources → needs
discussions: readDo NOT downgrade existing write scopes — only ADD the missing read
scope. If the workflow already has
issues: writeorpull-requests: write, read is already included; do nothing.(b)
GITHUB_TOKENavailable to the agent step. Check the step'senv:block. IfGITHUB_TOKEN(orGH_TOKEN) is not set, add:env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
If the workflow uses a GitHub App token (
steps.app-token.outputs.token)or a PAT, keep that existing token — do not replace it.
(c) Shell / Bash tool enabled on the agent action. Only relevant for
actions with explicit tool allowlists:
-
anthropics/claude-code-action/claude-code-base-action: ensureallowed_tools:includesBash(or specificallyBash(gh *)if youwant to be stricter). If
allowed_toolsis missing entirely, theagent has all tools by default — no change needed.
-
google-github-actions/run-gemini-cli: shell execution is enabledby default — no change needed.
-
openai/codex-action: ensure the action has shell access (checkthe action's parameters if relevant).
Only add what is missing. If a prerequisite is already satisfied, do not
touch it.
What NOT to do
"[content omitted]" — this destroys the workflow's purpose.
env:and then reference$VARin the prompt — the LLM still receives the same bytes. Thispattern only mitigates shell injection, not prompt injection.
read. If you need to add a scopeto
permissions:, add the minimum (issues: read, notissues: write).Attacker-controlled expressions in this workflow
The following expressions are attacker-controlled when interpolated into an
AI prompt. Apply Fix 2 to each of them:
${{ github.event.issue.title }}→ use${{ github.event.issue.number }}Additionally, scan the workflow YAML for any OTHER
github.event.*orgithub.head_refexpressions that appear insideprompt:,instructions:,or
run:fields — sibling taint sources in the same prompt (e.g.issue.title next to issue.body) may not be listed above but must also be
rewritten using the same ID-based pattern.
Changes
.github/workflows/gemini-issue-triage.ymlValidation
Closes #152