Skip to content

Commit bad9f85

Browse files
authored
fix: prevent command injection in claude-code-review workflow (#327)
Replace direct ${{ }} expression interpolation with environment variables in all `run:` blocks to prevent shell command injection via attacker-controlled PR filenames, commit messages, and author names. Fixes #325
1 parent fa1c669 commit bad9f85

1 file changed

Lines changed: 94 additions & 50 deletions

File tree

.github/workflows/claude-code-review.yml

Lines changed: 94 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -148,10 +148,14 @@ jobs:
148148
id: pr-checkout-info
149149
if: github.event_name == 'issue_comment'
150150
timeout-minutes: 2
151+
env:
152+
ISSUE_NUMBER: ${{ github.event.issue.number }}
153+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
154+
REPO_NAME: ${{ github.repository }}
151155
run: |
152156
set -euo pipefail
153157
154-
PR_NUMBER="${{ github.event.issue.number }}"
158+
PR_NUMBER="$ISSUE_NUMBER"
155159
156160
# Validate PR number
157161
if ! [[ "$PR_NUMBER" =~ ^[0-9]+$ ]]; then
@@ -164,9 +168,9 @@ jobs:
164168
# Get PR head ref with retry logic
165169
for attempt in {1..3}; do
166170
if PR_DATA=$(curl -sS --max-time 30 --retry 2 \
167-
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
171+
-H "Authorization: token $GITHUB_TOKEN" \
168172
-H "Accept: application/vnd.github.v3+json" \
169-
"https://api.github.com/repos/${{ github.repository }}/pulls/$PR_NUMBER"); then
173+
"https://api.github.com/repos/${REPO_NAME}/pulls/$PR_NUMBER"); then
170174
171175
if echo "$PR_DATA" | jq empty 2>/dev/null; then
172176
HEAD_REF=$(echo "$PR_DATA" | jq -r '.head.ref // empty')
@@ -201,6 +205,10 @@ jobs:
201205
- name: Refresh git state
202206
id: git-refresh
203207
timeout-minutes: 3
208+
env:
209+
EVENT_NAME: ${{ github.event_name }}
210+
PR_HEAD_SHA: ${{ steps.pr-checkout-info.outputs.pr_head_sha }}
211+
PR_HEAD_REF: ${{ steps.pr-checkout-info.outputs.pr_head_ref }}
204212
run: |
205213
set -euo pipefail
206214
@@ -218,19 +226,18 @@ jobs:
218226
git fetch --all --prune
219227
220228
# For issue_comment events, ensure we're on the latest commit of the PR branch
221-
if [ "${{ github.event_name }}" = "issue_comment" ]; then
222-
EXPECTED_SHA="${{ steps.pr-checkout-info.outputs.pr_head_sha }}"
229+
if [ "$EVENT_NAME" = "issue_comment" ]; then
230+
EXPECTED_SHA="$PR_HEAD_SHA"
223231
echo "Expected SHA from PR: $EXPECTED_SHA"
224232
225233
# Fetch the specific PR branch to get latest changes
226-
PR_HEAD_REF="${{ steps.pr-checkout-info.outputs.pr_head_ref }}"
227234
if [ -n "$PR_HEAD_REF" ]; then
228-
echo "Fetching latest changes for PR branch: $PR_HEAD_REF"
229-
git fetch origin "$PR_HEAD_REF:$PR_HEAD_REF" 2>/dev/null || true
235+
echo "Fetching latest changes for PR branch: ${PR_HEAD_REF}"
236+
git fetch origin "${PR_HEAD_REF}:${PR_HEAD_REF}" 2>/dev/null || true
230237
231238
# Reset to the latest commit on the PR branch
232-
echo "Resetting to latest commit on $PR_HEAD_REF"
233-
git reset --hard "origin/$PR_HEAD_REF"
239+
echo "Resetting to latest commit on ${PR_HEAD_REF}"
240+
git reset --hard "origin/${PR_HEAD_REF}"
234241
235242
NEW_SHA=$(git rev-parse HEAD)
236243
echo "Updated to SHA: $NEW_SHA"
@@ -273,6 +280,7 @@ jobs:
273280
timeout-minutes: 2
274281
env:
275282
COMMENT_BODY: ${{ github.event.comment.body }}
283+
EVENT_NAME: ${{ github.event_name }}
276284
run: |
277285
set -euo pipefail # Enhanced error handling
278286
@@ -293,7 +301,7 @@ jobs:
293301
echo "full_analysis=false" >> $GITHUB_OUTPUT
294302
295303
# Parse comment content for commands (using safe variable)
296-
if [ "${{ github.event_name }}" == "issue_comment" ] || [ "${{ github.event_name }}" == "pull_request_review_comment" ]; then
304+
if [ "$EVENT_NAME" == "issue_comment" ] || [ "$EVENT_NAME" == "pull_request_review_comment" ]; then
297305
# Use grep with fixed strings where possible for security
298306
if echo "$SAFE_COMMENT" | grep -qiF "codebot hunt"; then
299307
echo "mode=hunt" >> $GITHUB_OUTPUT
@@ -349,6 +357,13 @@ jobs:
349357
- name: Get PR information
350358
id: pr-info
351359
timeout-minutes: 3
360+
env:
361+
EVENT_NAME: ${{ github.event_name }}
362+
PR_EVENT_NUMBER: ${{ github.event.pull_request.number }}
363+
PR_EVENT_BASE_REF: ${{ github.event.pull_request.base.ref }}
364+
ISSUE_EVENT_NUMBER: ${{ github.event.issue.number }}
365+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
366+
REPO_NAME: ${{ github.repository }}
352367
run: |
353368
set -euo pipefail # Enhanced error handling
354369
@@ -357,11 +372,11 @@ jobs:
357372
BASE_REF=""
358373
359374
# Handle different trigger types with validation
360-
if [ "${{ github.event_name }}" = "pull_request" ]; then
361-
PR_NUMBER="${{ github.event.pull_request.number }}"
362-
BASE_REF="${{ github.event.pull_request.base.ref }}"
363-
elif [ "${{ github.event_name }}" = "issue_comment" ]; then
364-
PR_NUMBER="${{ github.event.issue.number }}"
375+
if [ "$EVENT_NAME" = "pull_request" ]; then
376+
PR_NUMBER="$PR_EVENT_NUMBER"
377+
BASE_REF="$PR_EVENT_BASE_REF"
378+
elif [ "$EVENT_NAME" = "issue_comment" ]; then
379+
PR_NUMBER="$ISSUE_EVENT_NUMBER"
365380
366381
# Validate PR number is numeric
367382
if ! [[ "$PR_NUMBER" =~ ^[0-9]+$ ]]; then
@@ -373,9 +388,9 @@ jobs:
373388
for attempt in {1..3}; do
374389
echo "Attempt $attempt: Fetching PR data for PR #$PR_NUMBER"
375390
if PR_DATA=$(curl -sS --max-time 30 --retry 2 \
376-
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
391+
-H "Authorization: token $GITHUB_TOKEN" \
377392
-H "Accept: application/vnd.github.v3+json" \
378-
"https://api.github.com/repos/${{ github.repository }}/pulls/$PR_NUMBER"); then
393+
"https://api.github.com/repos/${REPO_NAME}/pulls/$PR_NUMBER"); then
379394
380395
# Validate JSON response
381396
if echo "$PR_DATA" | jq empty 2>/dev/null; then
@@ -396,11 +411,11 @@ jobs:
396411
sleep $((2 ** attempt))
397412
done
398413
399-
elif [ "${{ github.event_name }}" = "pull_request_review_comment" ]; then
400-
PR_NUMBER="${{ github.event.pull_request.number }}"
401-
BASE_REF="${{ github.event.pull_request.base.ref }}"
414+
elif [ "$EVENT_NAME" = "pull_request_review_comment" ]; then
415+
PR_NUMBER="$PR_EVENT_NUMBER"
416+
BASE_REF="$PR_EVENT_BASE_REF"
402417
else
403-
echo "Error: Unsupported event type: ${{ github.event_name }}"
418+
echo "Error: Unsupported event type: $EVENT_NAME"
404419
exit 1
405420
fi
406421
@@ -424,14 +439,14 @@ jobs:
424439
- name: Get changed files
425440
id: changes
426441
if: steps.parse-command.outputs.full_analysis == 'false'
442+
env:
443+
BASE_REF: ${{ steps.pr-info.outputs.base_ref }}
444+
CURRENT_SHA: ${{ steps.git-refresh.outputs.current_sha }}
445+
CURRENT_BRANCH: ${{ steps.git-refresh.outputs.current_branch }}
427446
run: |
428447
429448
set -euo pipefail
430449
431-
BASE_REF="${{ steps.pr-info.outputs.base_ref }}"
432-
CURRENT_SHA="${{ steps.git-refresh.outputs.current_sha }}"
433-
CURRENT_BRANCH="${{ steps.git-refresh.outputs.current_branch }}"
434-
435450
echo "🔍 Detecting changed files for analysis"
436451
echo "Base branch: $BASE_REF"
437452
echo "Current SHA: $CURRENT_SHA"
@@ -533,14 +548,19 @@ jobs:
533548
- name: Verify commit SHA and git state
534549
id: verify-state
535550
timeout-minutes: 2
551+
env:
552+
REFRESHED_SHA: ${{ steps.git-refresh.outputs.current_sha }}
553+
REFRESHED_BRANCH: ${{ steps.git-refresh.outputs.current_branch }}
554+
REPO_NAME: ${{ github.repository }}
555+
EVENT_NAME: ${{ github.event_name }}
536556
run: |
537557
set -euo pipefail
538558
539559
echo "🔍 Verifying git state before analysis"
540560
541-
CURRENT_SHA="${{ steps.git-refresh.outputs.current_sha }}"
561+
CURRENT_SHA="$REFRESHED_SHA"
542562
ACTUAL_SHA=$(git rev-parse HEAD)
543-
CURRENT_BRANCH="${{ steps.git-refresh.outputs.current_branch }}"
563+
CURRENT_BRANCH="$REFRESHED_BRANCH"
544564
545565
# Verify SHA consistency
546566
if [ "$CURRENT_SHA" != "$ACTUAL_SHA" ]; then
@@ -561,8 +581,8 @@ jobs:
561581
echo "📊 Git state summary:"
562582
echo " Commit SHA: $CURRENT_SHA"
563583
echo " Branch: $CURRENT_BRANCH"
564-
echo " Repository: ${{ github.repository }}"
565-
echo " Event: ${{ github.event_name }}"
584+
echo " Repository: $REPO_NAME"
585+
echo " Event: $EVENT_NAME"
566586
567587
# Get commit info for better context
568588
COMMIT_MSG=$(git log -1 --pretty=format:"%s" 2>/dev/null || echo "Unable to get commit message")
@@ -574,7 +594,7 @@ jobs:
574594
echo " Date: $COMMIT_DATE"
575595
576596
# Create unique identifier for cache invalidation
577-
REVIEW_ID="${CURRENT_SHA:0:8}-${{ github.event_name }}-$(date +%s)"
597+
REVIEW_ID="${CURRENT_SHA:0:8}-${EVENT_NAME}-$(date +%s)"
578598
echo " Review ID: $REVIEW_ID"
579599
580600
# Output additional context
@@ -669,6 +689,25 @@ jobs:
669689
- name: Workflow Summary
670690
if: always()
671691
timeout-minutes: 2
692+
env:
693+
# Pass all step outputs via env vars to prevent command injection
694+
# (attacker-controlled data like commit messages and filenames must never
695+
# be interpolated directly into shell commands via ${{ }})
696+
REVIEW_MODE: ${{ steps.parse-command.outputs.mode }}
697+
FOCUS_AREAS: ${{ steps.parse-command.outputs.focus }}
698+
VERBOSE_OUTPUT: ${{ steps.parse-command.outputs.verbose }}
699+
FULL_ANALYSIS: ${{ steps.parse-command.outputs.full_analysis || 'false' }}
700+
CHANGED_FILES: ${{ steps.changes.outputs.changed_files }}
701+
CHANGED_COUNT: ${{ steps.changes.outputs.changed_count || '0' }}
702+
DIFF_SUCCESSFUL: ${{ steps.changes.outputs.diff_successful }}
703+
PR_NUMBER: ${{ steps.pr-info.outputs.pr_number || 'N/A' }}
704+
BASE_BRANCH: ${{ steps.pr-info.outputs.base_ref || 'N/A' }}
705+
COMMIT_SHA: ${{ steps.verify-state.outputs.current_sha || steps.git-refresh.outputs.current_sha || 'Unknown' }}
706+
REVIEW_ID: ${{ steps.verify-state.outputs.review_id || 'N/A' }}
707+
CURRENT_BRANCH: ${{ steps.git-refresh.outputs.current_branch || 'Unknown' }}
708+
COMMIT_MESSAGE: ${{ steps.verify-state.outputs.commit_message || 'Unknown' }}
709+
COMMIT_AUTHOR: ${{ steps.verify-state.outputs.commit_author || 'Unknown' }}
710+
COMMIT_DATE: ${{ steps.verify-state.outputs.commit_date || 'Unknown' }}
672711
run: |
673712
set -euo pipefail # Enhanced error handling
674713
@@ -685,35 +724,40 @@ jobs:
685724
686725
# Review Configuration
687726
echo "### ⚙️ Review Configuration" >> $GITHUB_STEP_SUMMARY
688-
echo "**Review Mode:** \`${{ steps.parse-command.outputs.mode }}\`" >> $GITHUB_STEP_SUMMARY
689-
echo "**Focus Areas:** \`${{ steps.parse-command.outputs.focus }}\`" >> $GITHUB_STEP_SUMMARY
690-
echo "**Verbose Output:** \`${{ steps.parse-command.outputs.verbose }}\`" >> $GITHUB_STEP_SUMMARY
691-
echo "**Full Analysis:** \`${{ steps.parse-command.outputs.full_analysis || 'false' }}\`" >> $GITHUB_STEP_SUMMARY
692-
if [ "${{ steps.parse-command.outputs.full_analysis }}" != "true" ]; then
693-
echo "**Changed Files:** \`${{ steps.changes.outputs.changed_count || '0' }}\` files" >> $GITHUB_STEP_SUMMARY
694-
echo "**Diff Detection:** \`${{ steps.changes.outputs.diff_successful == 'true' && 'Successful' || 'Failed' }}\`" >> $GITHUB_STEP_SUMMARY
727+
echo "**Review Mode:** \`${REVIEW_MODE}\`" >> $GITHUB_STEP_SUMMARY
728+
echo "**Focus Areas:** \`${FOCUS_AREAS}\`" >> $GITHUB_STEP_SUMMARY
729+
echo "**Verbose Output:** \`${VERBOSE_OUTPUT}\`" >> $GITHUB_STEP_SUMMARY
730+
echo "**Full Analysis:** \`${FULL_ANALYSIS}\`" >> $GITHUB_STEP_SUMMARY
731+
if [ "$FULL_ANALYSIS" != "true" ]; then
732+
DIFF_STATUS="Failed"
733+
if [ "$DIFF_SUCCESSFUL" = "true" ]; then
734+
DIFF_STATUS="Successful"
735+
fi
736+
echo "**Changed Files:** \`${CHANGED_COUNT}\` files" >> $GITHUB_STEP_SUMMARY
737+
echo "**Diff Detection:** \`${DIFF_STATUS}\`" >> $GITHUB_STEP_SUMMARY
695738
fi
696-
echo "**PR Number:** \`${{ steps.pr-info.outputs.pr_number || 'N/A' }}\`" >> $GITHUB_STEP_SUMMARY
697-
echo "**Base Branch:** \`${{ steps.pr-info.outputs.base_ref || 'N/A' }}\`" >> $GITHUB_STEP_SUMMARY
739+
echo "**PR Number:** \`${PR_NUMBER}\`" >> $GITHUB_STEP_SUMMARY
740+
echo "**Base Branch:** \`${BASE_BRANCH}\`" >> $GITHUB_STEP_SUMMARY
698741
echo "" >> $GITHUB_STEP_SUMMARY
699742
700743
# Git State Information
701744
echo "### 📊 Git State Analysis" >> $GITHUB_STEP_SUMMARY
702-
echo "**Commit SHA:** \`${{ steps.verify-state.outputs.current_sha || steps.git-refresh.outputs.current_sha || 'Unknown' }}\`" >> $GITHUB_STEP_SUMMARY
703-
echo "**Review ID:** \`${{ steps.verify-state.outputs.review_id || 'N/A' }}\`" >> $GITHUB_STEP_SUMMARY
704-
echo "**Current Branch:** \`${{ steps.git-refresh.outputs.current_branch || 'Unknown' }}\`" >> $GITHUB_STEP_SUMMARY
705-
echo "**Last Commit:** \"${{ steps.verify-state.outputs.commit_message || 'Unknown' }}\"" >> $GITHUB_STEP_SUMMARY
706-
echo "**Author:** \`${{ steps.verify-state.outputs.commit_author || 'Unknown' }}\`" >> $GITHUB_STEP_SUMMARY
707-
echo "**Date:** \`${{ steps.verify-state.outputs.commit_date || 'Unknown' }}\`" >> $GITHUB_STEP_SUMMARY
745+
echo "**Commit SHA:** \`${COMMIT_SHA}\`" >> $GITHUB_STEP_SUMMARY
746+
echo "**Review ID:** \`${REVIEW_ID}\`" >> $GITHUB_STEP_SUMMARY
747+
echo "**Current Branch:** \`${CURRENT_BRANCH}\`" >> $GITHUB_STEP_SUMMARY
748+
# Use printf to safely handle special characters in commit messages
749+
printf '**Last Commit:** "%s"\n' "$COMMIT_MESSAGE" >> $GITHUB_STEP_SUMMARY
750+
printf '**Author:** `%s`\n' "$COMMIT_AUTHOR" >> $GITHUB_STEP_SUMMARY
751+
echo "**Date:** \`${COMMIT_DATE}\`" >> $GITHUB_STEP_SUMMARY
708752
echo "" >> $GITHUB_STEP_SUMMARY
709753
710754
# Analysis Scope Details
711-
if [ "${{ steps.parse-command.outputs.full_analysis }}" != "true" ] && [ "${{ steps.changes.outputs.changed_count || '0' }}" -gt "0" ]; then
755+
if [ "$FULL_ANALYSIS" != "true" ] && [ "${CHANGED_COUNT:-0}" -gt "0" ] 2>/dev/null; then
712756
echo "### 📄 Files Being Analyzed" >> $GITHUB_STEP_SUMMARY
713757
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
714-
echo "${{ steps.changes.outputs.changed_files }}" | tr ' ' '\n' | grep -v '^$' | head -20 >> $GITHUB_STEP_SUMMARY
715-
if [ "${{ steps.changes.outputs.changed_count }}" -gt "20" ]; then
716-
echo "... and $((${{ steps.changes.outputs.changed_count }} - 20)) more files" >> $GITHUB_STEP_SUMMARY
758+
echo "$CHANGED_FILES" | tr ' ' '\n' | grep -v '^$' | head -20 >> $GITHUB_STEP_SUMMARY
759+
if [ "${CHANGED_COUNT:-0}" -gt "20" ] 2>/dev/null; then
760+
echo "... and $((CHANGED_COUNT - 20)) more files" >> $GITHUB_STEP_SUMMARY
717761
fi
718762
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
719763
echo "" >> $GITHUB_STEP_SUMMARY

0 commit comments

Comments
 (0)