Skip to content

fix: harden CI workflows against prompt injection and supply chain risks#852

Merged
stack72 merged 1 commit intomainfrom
ci-hardening
Mar 25, 2026
Merged

fix: harden CI workflows against prompt injection and supply chain risks#852
stack72 merged 1 commit intomainfrom
ci-hardening

Conversation

@stack72
Copy link
Copy Markdown
Contributor

@stack72 stack72 commented Mar 25, 2026

  • Remove attacker-controlled data (issue title, body, comments) from Claude prompts in issue-triage.yml — Claude now fetches issue context itself via gh issue view
  • Restrict issue-triage.yml tool access from broad Bash(gh api:*) to specific safe endpoints
  • Add prompt injection hardening preamble to all Claude review jobs in ci.yml
  • Replace curl | bash actionlint install with SHA-pinned GitHub Action (v1.7.11)
  • Scope ci.yml permissions from workflow-level to per-job (least privilege)
  • Add new claude-ci-security-review job that audits workflow changes for prompt injection, expression injection, supply chain risks, and permission issues
  • Remove unused close-fork-prs.yml workflow
  • Remove unnecessary Setup Claude environment steps

Test plan

  • Open a test issue and run /triage — verify Claude fetches context itself and produces a valid triage
  • Open a test PR touching src/ — verify all three existing Claude review jobs still run
  • Open a test PR touching .github/workflows/ — verify the new CI security review job triggers
  • Verify non-CI PRs are not blocked by the skipped claude-ci-security-review job
  • Verify workflow-validation still runs actionlint via the pinned action

- Remove attacker-controlled data (issue title, body, comments) from Claude prompts in issue-triage.yml — Claude now fetches issue context itself via gh issue view
- Restrict issue-triage.yml tool access from broad Bash(gh api:*) to specific safe endpoints
- Add prompt injection hardening preamble to all Claude review jobs in ci.yml
- Replace curl | bash actionlint install with SHA-pinned GitHub Action (v1.7.11)
- Scope ci.yml permissions from workflow-level to per-job (least privilege)
- Add new claude-ci-security-review job that audits workflow changes for prompt injection, expression injection, supply chain risks, and permission issues
- Remove unused close-fork-prs.yml workflow
- Remove unnecessary Setup Claude environment steps

## Test plan

- Open a test issue and run /triage — verify Claude fetches context itself and produces a valid triage
- Open a test PR touching src/ — verify all three existing Claude review jobs still run
- Open a test PR touching .github/workflows/ — verify the new CI security review job triggers
- Verify non-CI PRs are not blocked by the skipped claude-ci-security-review job
- Verify workflow-validation still runs actionlint via the pinned action
Copy link
Copy Markdown

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Blocking Issues

None.

Suggestions

  1. Pre-existing: third-party actions not SHA-pinneddorny/paths-filter@v3 and anthropics/claude-code-action@v1 in ci.yml are pinned by tag, not commit SHA. The new rhysd/actionlint pin is correctly done. Consider SHA-pinning the others in a follow-up for consistency with the security posture this PR establishes. (Not blocking since these are pre-existing and unchanged.)

  2. Pre-existing: issue-triage.yml uses workflow-level permissions — Lines 7-9 have permissions at workflow level rather than job level. Since there's only one job this is functionally equivalent, but it's inconsistent with the per-job approach applied to ci.yml. Could be aligned in a follow-up.

  3. Deletion of close-fork-prs.yml — This removed the automatic closing of fork PRs with a contributor-friendly message. The auto-merge job already excludes forks, so fork PRs won't merge, but they will now sit open instead of being closed with guidance. Verify this is the intended behavior — the contributor experience message was useful.

Assessment

Excellent security hardening PR. The key improvements are:

  • Prompt injection fix: Removing direct interpolation of github.event.issue.title, github.event.issue.body, and comment thread into LLM prompts in issue-triage.yml — Claude now fetches this data itself via tool calls
  • Tool scoping: Replacing Bash(gh api:*) with specific endpoint restrictions prevents abuse of broad API access
  • Least privilege: Moving permissions from workflow-level to per-job ensures test/lint jobs only get contents: read
  • Supply chain: Replacing curl | bash with SHA-pinned rhysd/actionlint@393031ad eliminates remote script execution risk
  • Defense in depth: Adding prompt injection security preambles to all Claude review prompts and a dedicated CI security review job

Copy link
Copy Markdown

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI Security Review

Critical / High

No critical or high severity findings. This PR is a substantial security hardening of the CI pipeline.

Medium

  1. Pre-existing: Unpinned third-party actionsdorny/paths-filter@v3 (ci.yml:25), anthropics/claude-code-action@v1 (multiple locations), and denoland/setup-deno@v2 (multiple locations) use tag-based pins rather than full SHA pins. A compromised tag could inject malicious code. These are pre-existing (not introduced by this PR) but worth tracking for a future pinning pass. actions/* (GitHub-owned) with tag pins are acceptable per policy.

  2. Pre-existing: issue-triage.yml uses workflow-level permissions (line 7-9) rather than job-level. Since there's only one job, this is functionally equivalent, but job-level scoping is the stronger convention established by this PR in ci.yml. Consider aligning in a follow-up.

Low

None.

Verdict

PASS — This PR significantly improves CI security across multiple dimensions:

  • Prompt injection fix: Removes direct interpolation of attacker-controlled issue title/body/comments into the triage LLM prompt (previously a critical vulnerability). Claude now fetches data itself via tool calls.
  • Tool scope tightening: Narrows Bash(gh api:*) (which allowed arbitrary GitHub API calls) down to specific allowed commands.
  • Prompt hardening: Adds security preambles to all LLM prompts instructing the model to treat fetched content as untrusted.
  • Permission scoping: Moves from overly broad workflow-level permissions (including unnecessary id-token: write) to minimal job-level permissions.
  • Supply chain fix: Replaces curl | bash remote script execution with a SHA-pinned action.
  • New CI security review gate: Adds automated security review for workflow changes.

@stack72 stack72 merged commit 8eab7fe into main Mar 25, 2026
11 checks passed
@stack72 stack72 deleted the ci-hardening branch March 25, 2026 09:16
keeb added a commit that referenced this pull request Mar 28, 2026
## Summary

- Fix `/triage` command failure caused by `shell-quote` parsing of
unquoted `Bash(...)` patterns in `claude_args`
- Wraps the `--allowedTools` value in single quotes so spaces and
parentheses are preserved as a single token

## Root Cause

`claude-code-action` uses `shell-quote` to parse `claude_args`. The
`Bash(...)` patterns added in #852 contain spaces (e.g. `Bash(gh api
--method POST:*/reactions)`), which caused:
1. Parentheses treated as shell operators and stripped
2. Space-splitting broke patterns into separate tokens
3. `--method` parsed as a standalone CLI flag, corrupting the entire
argument structure
4. Claude Code crashed with exit code 1

Confirmed by workflow logs from the failed #902 triage — allowed tools
were `["Read", "Glob", "Grep", "Bash", "gh", "issue", "api"]` instead of
the intended patterns.

Fixes #904

## Test plan

- [ ] Re-run `/triage` on #902 after merge and verify it completes
successfully
- [ ] Verify the workflow logs show the correct `Bash(...)` patterns in
`allowedTools`

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant