Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions examples/ci-failure-auto-fix.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
name: Auto Fix CI Failures

# ⚠️ SECURITY NOTE
#
# This workflow checks out the PR branch and runs build/test commands
# (npm, bun, etc.) against it with elevated permissions (contents:write,
# id-token:write). This means code from the PR branch executes in a
# trusted context with access to secrets and the ability to push to the
# repository.
#
# Only use this workflow in repositories where everyone with write access
# is fully trusted with these permissions. Do not use this in repositories
# that accept contributions from untrusted or semi-trusted collaborators.
#
# The pull_requests[0] check below limits this to same-repo PRs (fork PRs
# are excluded), but anyone who can push a branch to this repository can
# control what code runs here.

on:
workflow_run:
workflows: ["CI"]
Expand Down Expand Up @@ -35,10 +51,14 @@ jobs:

- name: Create fix branch
id: branch
env:
HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
RUN_ID: ${{ github.run_id }}
run: |
BRANCH_NAME="claude-auto-fix-ci-${{ github.event.workflow_run.head_branch }}-${{ github.run_id }}"
SAFE_BRANCH=$(printf '%s' "$HEAD_BRANCH" | tr -cd 'a-zA-Z0-9/_.-')
BRANCH_NAME="claude-auto-fix-ci-${SAFE_BRANCH}-${RUN_ID}"
git checkout -b "$BRANCH_NAME"
echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
echo "branch_name=$BRANCH_NAME" >> "$GITHUB_OUTPUT"

- name: Get CI failure details
id: failure_details
Expand Down
8 changes: 5 additions & 3 deletions examples/test-failure-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ jobs:
fromJSON(steps.detect.outputs.structured_output).confidence >= 0.7
env:
GH_TOKEN: ${{ github.token }}
WORKFLOW_NAME: ${{ github.event.workflow_run.name }}
HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
run: |
OUTPUT='${{ steps.detect.outputs.structured_output }}'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟣 Pre-existing issue: OUTPUT='${{ steps.detect.outputs.structured_output }}' on lines 59, 76, and 89 is directly interpolated into shell using single quotes — the same class of vulnerability this PR fixes for workflow_run values. If Claude's JSON output contains a single quote (e.g., "summary": "It's a flaky test"), the single-quoting breaks shell parsing. Since the PR already adds env: blocks to these steps, consider also passing structured_output via env: for consistency.

Extended reasoning...

What the bug is

In examples/test-failure-analysis.yml, the variable structured_output from the Claude action step is assigned in shell using single-quote interpolation:

run: |
  OUTPUT='${{ steps.detect.outputs.structured_output }}'

This appears on lines 59, 76, and 89. GitHub Actions evaluates the ${{ }} expression and substitutes the raw value directly into the shell script before bash executes it.

How it manifests

The structured_output is JSON produced by Claude with a schema containing a summary string field. If that string contains a single quote — which is entirely plausible for natural language (e.g., "summary": "It's a flaky test") — the resulting shell line becomes:

OUTPUT='{ "summary": "It's a flaky test" }'

The single quote inside It's terminates the single-quoted string prematurely, leaving s a flaky test" }' as unquoted shell text. This causes a shell syntax error at minimum, and in adversarial scenarios could lead to command injection.

Why existing code doesn't prevent it

The JSON schema constrains the structure of the output but not the characters within string fields. Claude can produce any valid string content in the summary field. There is no escaping or sanitization applied between the GitHub Actions expression evaluation and shell execution.

Impact

In practice, the impact is a robustness issue rather than a security vulnerability, since structured_output comes from the repo's own Claude invocation rather than attacker-controlled input like branch names. However, it would cause the workflow to fail unexpectedly when Claude's summary contains apostrophes or single quotes — a common occurrence in English text.

How to fix

Pass structured_output through an environment variable, the same pattern this PR applies to workflow_run values:

env:
  STRUCTURED_OUTPUT: ${{ steps.detect.outputs.structured_output }}
run: |
  OUTPUT="$STRUCTURED_OUTPUT"

This ensures the value is treated as a literal string by the shell regardless of its contents.

Pre-existing status

This pattern existed before this PR and is not introduced or modified by it. However, the PR already touches the env: blocks of these exact steps to add WORKFLOW_NAME and HEAD_BRANCH, so fixing structured_output at the same time would be a natural and low-risk addition for consistency.

CONFIDENCE=$(echo "$OUTPUT" | jq -r '.confidence')
Expand All @@ -63,8 +65,7 @@ jobs:
echo ""
echo "Triggering automatic retry..."

gh workflow run "${{ github.event.workflow_run.name }}" \
--ref "${{ github.event.workflow_run.head_branch }}"
gh workflow run "$WORKFLOW_NAME" --ref "$HEAD_BRANCH"

# Low confidence flaky detection - skip retry
- name: Low confidence detection
Expand All @@ -83,13 +84,14 @@ jobs:
if: github.event.workflow_run.event == 'pull_request'
env:
GH_TOKEN: ${{ github.token }}
HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
run: |
OUTPUT='${{ steps.detect.outputs.structured_output }}'
IS_FLAKY=$(echo "$OUTPUT" | jq -r '.is_flaky')
CONFIDENCE=$(echo "$OUTPUT" | jq -r '.confidence')
SUMMARY=$(echo "$OUTPUT" | jq -r '.summary')

pr_number=$(gh pr list --head "${{ github.event.workflow_run.head_branch }}" --json number --jq '.[0].number')
pr_number=$(gh pr list --head "$HEAD_BRANCH" --json number --jq '.[0].number')

if [ -n "$pr_number" ]; then
if [ "$IS_FLAKY" = "true" ]; then
Comment thread
ddworken marked this conversation as resolved.
Expand Down
Loading