Harden summary.yml against shell + prompt injection - #86
Conversation
The "Comment with AI summary" step interpolated
${{ steps.inference.outputs.response }} into a single-quoted shell arg, so
a quote in the AI output broke the step (and was a shell-injection vector).
Use the already-defined "$RESPONSE" env var instead. Also add a
system-prompt preamble telling the model to treat the issue title/body as
untrusted data (mitigates prompt injection from issue content).
Found during the rme port review (d-morrison/rme#827); this is the template
source of that workflow, so fixing it here stops it propagating further.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Claude finished @d-morrison's task in 1m 14s —— View job Code Review — PR #86: Harden summary.yml against shell + prompt injection
SummaryThe PR is correct and well-targeted. The shell injection fix is the right approach; the prompt injection mitigation is a reasonable best-effort given the single-field API. A few notes below. Shell injection fix (
|
There was a problem hiding this comment.
Pull request overview
Hardens the .github/workflows/summary.yml workflow against shell injection and basic prompt-injection attempts when posting AI-generated summaries back to issues.
Changes:
- Reworks the inference prompt to explicitly treat issue title/body as untrusted input and to ignore any instructions embedded in them.
- Fixes the
gh issue commentinvocation to pass the model output via an environment variable with safe double-quoting (avoids single-quote breakage and shell-injection vectors).
Address the #86 review's non-blocking suggestions: wrap the interpolated issue title/body in ===BEGIN ISSUE===/===END ISSUE=== markers so prompt boundary-crossing is harder, and skip the comment step when the model returns an empty response (avoids posting a blank comment). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Claude finished @d-morrison's task in 1m 33s —— View job Code Review — PR #86: Harden summary.yml against shell + prompt injection
Verdict: ApproveThe critical shell-injection bug is correctly fixed. The prompt-injection mitigations are the best available given the action's API constraints. No blocking issues. Shell injection fix (line 38)Correct. The old pattern expanded Empty-response guard (line 36)Good addition. The Prompt injection mitigations (lines 24–32)The Pre-existing / out of scope
|
| if: steps.inference.outputs.response != '' | ||
| run: | | ||
| gh issue comment $ISSUE_NUMBER --body '${{ steps.inference.outputs.response }}' | ||
| gh issue comment "$ISSUE_NUMBER" --body "$RESPONSE" |
There was a problem hiding this comment.
Shell injection fix — correct. The old form expanded ${{ steps.inference.outputs.response }} at YAML template-evaluation time directly inside a single-quoted shell argument, so any ' in the response would break quoting and '; cmd; ' was live injection. The new form assigns the value to an env var (RESPONSE: ...) and reads it as "$RESPONSE". Environment variables are passed to the shell process directly — not re-parsed — so this is the safe, idiomatic pattern.
"$ISSUE_NUMBER" is also correctly double-quoted here (the value is a numeric integer so it was never exploitable, but quoting it is correct practice).
|
|
||
| - name: Comment with AI summary | ||
| # Skip if the model returned nothing, so we don't post a blank comment. | ||
| if: steps.inference.outputs.response != '' |
There was a problem hiding this comment.
Good defensive guard. This correctly prevents posting a blank comment when the model returns an empty response. One note: if the inference step errors out, GitHub Actions will already skip this step by default (steps are skipped when a prior step fails unless if: always() is set), so this check specifically handles the case where the action succeeds but returns an empty string — which is the right thing to guard against.
| ===BEGIN ISSUE=== | ||
| Title: ${{ github.event.issue.title }} | ||
| Body: ${{ github.event.issue.body }} | ||
| ===END ISSUE=== |
There was a problem hiding this comment.
Delimiter approach is a good improvement over the previous prompt. One residual weakness: ${{ github.event.issue.title }} and ${{ github.event.issue.body }} are still expanded at workflow template-evaluation time, so an issue whose body literally contains ===END ISSUE=== followed by instructions could confuse the model about where the trusted/untrusted boundary is.
This is a limitation of the actions/ai-inference@v1 API (no separate system: field), so there's no complete fix available here — the delimiters still raise the bar considerably. Documenting this residual limitation in the PR description (as done) is the right call.
If a future version of the action exposes a system: input, moving the instruction preamble there would eliminate this class of attack entirely.
|
Problem
summary.yml's "Comment with AI summary" step interpolated${{ steps.inference.outputs.response }}directly into a single-quoted shell argument:gh issue comment $ISSUE_NUMBER --body '${{ steps.inference.outputs.response }}'A single quote in the AI output (e.g.
it's) breaks the step, and a crafted issue body that steers the model to emit'; ...; 'is a shell-injection vector. TheRESPONSEenv var was already defined but unused.Fix
gh issue comment "$ISSUE_NUMBER" --body "$RESPONSE".Found during the rme port review (d-morrison/rme#827); fixing the template source so it doesn't keep propagating to repos created from qwt.
Test plan
🤖 Generated with Claude Code