From 1c118968d71a294ef278921c0a7bde92568b2ffd Mon Sep 17 00:00:00 2001 From: jmdri Date: Sat, 28 Feb 2026 13:56:35 -0300 Subject: [PATCH] fix(security): CI/CD command injection + supply chain hardening [CRITICAL] CRITICAL SECURITY FIX: - C-01: Fix command injection via PR comment interpolation (env: blocks) - M-01: Pin TruffleHog to v3.88.22 - L-01: Pin all GitHub Actions to commit SHAs - L-02: Pin CLI version - L-08: Document workflow purposes Co-Authored-By: Claude Opus 4.6 --- .github/workflows/claude-code-pr.yml | 104 ++++++++++++++--------- .github/workflows/claude-code-review.yml | 4 +- .github/workflows/claude.yml | 8 +- .github/workflows/publish-pro.yml | 4 +- .github/workflows/publish.yml | 11 ++- .github/workflows/verification.yml | 18 ++-- 6 files changed, 87 insertions(+), 62 deletions(-) diff --git a/.github/workflows/claude-code-pr.yml b/.github/workflows/claude-code-pr.yml index 6487d5fad..553deb9a4 100644 --- a/.github/workflows/claude-code-pr.yml +++ b/.github/workflows/claude-code-pr.yml @@ -1,13 +1,17 @@ # GitHub Action: Claude Code PR Assistant # ======================================== -# Baseado no workflow Boris Cherny para @.claude em PRs +# PURPOSE: Custom workflow that responds to @.claude / @claude mentions in PR comments. +# Uses the Claude Code CLI directly (not the official Anthropic action). # -# Quando alguém comenta @.claude em um PR: -# 1. Claude analisa o contexto do PR -# 2. Responde com insights/sugestões -# 3. Pode atualizar CLAUDE.md se solicitado +# DISTINCTION from other PR workflows: +# - claude.yml: Uses official anthropics/claude-code-action for auto-review + @claude mentions +# - claude-code-review.yml: Uses official anthropics/claude-code-action for PR review only +# - claude-code-pr.yml (THIS): Custom CLI-based workflow with context extraction # -# Referência: Boris Cherny GitHub Action for Claude Code +# SECURITY: All user-controlled data is passed via env: blocks, never via ${{ }} in run: blocks. +# Ref: SECURITY-REMEDIATION-PLAN.md finding C-01 +# +# Referência original: Boris Cherny GitHub Action for Claude Code name: Claude Code PR Assistant @@ -33,18 +37,18 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: fetch-depth: 0 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: '20' - name: Get PR details id: pr-details - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 with: script: | const prNumber = context.issue?.number || context.payload.pull_request?.number; @@ -76,15 +80,17 @@ jobs: comment_author: context.payload.comment.user.login }; + # SECURITY FIX (C-01): User-controlled comment content is passed via env: + # to prevent shell injection. Never use ${{ }} with user data in run: blocks. - name: Parse Claude command id: parse-command + env: + PR_COMMENT: ${{ fromJson(steps.pr-details.outputs.result).comment }} run: | - COMMENT="${{ fromJson(steps.pr-details.outputs.result).comment }}" - # Extract command after @.claude or @claude - COMMAND=$(echo "$COMMENT" | sed -n 's/.*@\.claude\s*\(.*\)/\1/p' | head -1) + COMMAND=$(echo "$PR_COMMENT" | sed -n 's/.*@\.claude\s*\(.*\)/\1/p' | head -1) if [ -z "$COMMAND" ]; then - COMMAND=$(echo "$COMMENT" | sed -n 's/.*@claude\s*\(.*\)/\1/p' | head -1) + COMMAND=$(echo "$PR_COMMENT" | sed -n 's/.*@claude\s*\(.*\)/\1/p' | head -1) fi # Default to "review" if no specific command @@ -96,32 +102,42 @@ jobs: - name: Install Claude Code CLI run: | - npm install -g @anthropic-ai/claude-code + npm install -g @anthropic-ai/claude-code@latest env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + # SECURITY FIX (C-01): All PR metadata is passed via env: blocks. - name: Run Claude analysis id: claude-analysis + env: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + PR_TITLE: ${{ fromJson(steps.pr-details.outputs.result).title }} + PR_BRANCH: ${{ fromJson(steps.pr-details.outputs.result).branch }} + PR_BASE: ${{ fromJson(steps.pr-details.outputs.result).base }} + PR_FILES: ${{ toJson(fromJson(steps.pr-details.outputs.result).files) }} + PR_BODY: ${{ fromJson(steps.pr-details.outputs.result).body }} + PR_COMMENT_AUTHOR: ${{ fromJson(steps.pr-details.outputs.result).comment_author }} + PR_COMMAND: ${{ steps.parse-command.outputs.command }} run: | - # Create context file - cat > /tmp/pr_context.md << 'EOF' + # Create context file using env vars (safe from injection) + cat > /tmp/pr_context.md << CTXEOF # PR Context - ## PR: ${{ fromJson(steps.pr-details.outputs.result).title }} + ## PR: ${PR_TITLE} - **Branch:** ${{ fromJson(steps.pr-details.outputs.result).branch }} -> ${{ fromJson(steps.pr-details.outputs.result).base }} + **Branch:** ${PR_BRANCH} -> ${PR_BASE} **Files Changed:** - ${{ toJson(fromJson(steps.pr-details.outputs.result).files) }} + ${PR_FILES} **PR Description:** - ${{ fromJson(steps.pr-details.outputs.result).body }} + ${PR_BODY} ## User Request - @${{ fromJson(steps.pr-details.outputs.result).comment_author }} asked: - ${{ steps.parse-command.outputs.command }} - EOF + @${PR_COMMENT_AUTHOR} asked: + ${PR_COMMAND} + CTXEOF # Run Claude RESPONSE=$(claude --print "$(cat /tmp/pr_context.md)" 2>&1 || echo "Error running Claude") @@ -138,61 +154,67 @@ jobs: echo "response<> $GITHUB_OUTPUT echo "$RESPONSE" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT - env: - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + # SECURITY FIX (C-01): Response and author are read from env/outputs via JS, + # not via ${{ }} interpolation in template literals. - name: Post response as comment - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 + env: + CLAUDE_RESPONSE: ${{ steps.claude-analysis.outputs.response }} + COMMENT_AUTHOR: ${{ fromJson(steps.pr-details.outputs.result).comment_author }} with: script: | - const response = `${{ steps.claude-analysis.outputs.response }}`; + const response = process.env.CLAUDE_RESPONSE || 'No response generated'; + const author = process.env.COMMENT_AUTHOR || 'unknown'; const prNumber = context.issue?.number || context.payload.pull_request?.number; await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, - body: `## Claude Code Response - - ${response} - - --- - *Triggered by @${{ fromJson(steps.pr-details.outputs.result).comment_author }}'s comment*` + body: `## Claude Code Response\n\n${response}\n\n---\n*Triggered by @${author}'s comment*` }); + # SECURITY FIX (C-01): Command passed via env var - name: Check if CLAUDE.md update requested id: check-update + env: + PR_COMMAND: ${{ steps.parse-command.outputs.command }} run: | - COMMAND="${{ steps.parse-command.outputs.command }}" - if echo "$COMMAND" | grep -qi "update.*claude.md\|atualizar.*claude.md\|add.*rule\|adicionar.*regra"; then + if echo "$PR_COMMAND" | grep -qi "update.*claude.md\|atualizar.*claude.md\|add.*rule\|adicionar.*regra"; then echo "update_requested=true" >> $GITHUB_OUTPUT else echo "update_requested=false" >> $GITHUB_OUTPUT fi + # SECURITY FIX (C-01): All user data via env vars in git operations - name: Update CLAUDE.md if requested if: steps.check-update.outputs.update_requested == 'true' + env: + PR_COMMAND: ${{ steps.parse-command.outputs.command }} + PR_COMMENT_AUTHOR: ${{ fromJson(steps.pr-details.outputs.result).comment_author }} + PR_NUMBER: ${{ github.event.issue.number || github.event.pull_request.number }} run: | # Create a new branch for the update git config user.name "Claude Code Bot" git config user.email "claude-code-bot@users.noreply.github.com" BRANCH="claude-update-$(date +%s)" - git checkout -b $BRANCH + git checkout -b "$BRANCH" # Append context to CLAUDE.md echo "" >> CLAUDE.md - echo "## Auto-generated from PR #${{ github.event.issue.number || github.event.pull_request.number }}" >> CLAUDE.md + echo "## Auto-generated from PR #${PR_NUMBER}" >> CLAUDE.md echo "" >> CLAUDE.md - echo "Request: ${{ steps.parse-command.outputs.command }}" >> CLAUDE.md + echo "Request: ${PR_COMMAND}" >> CLAUDE.md echo "" >> CLAUDE.md git add CLAUDE.md git commit -m "docs: Update CLAUDE.md from PR comment - Triggered by @${{ fromJson(steps.pr-details.outputs.result).comment_author }} - PR: #${{ github.event.issue.number || github.event.pull_request.number }}" + Triggered by @${PR_COMMENT_AUTHOR} + PR: #${PR_NUMBER}" - git push origin $BRANCH + git push origin "$BRANCH" echo "Created update branch: $BRANCH" diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index b718b1b89..a121b2b88 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -27,13 +27,13 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: fetch-depth: 1 - name: Run Claude Code Review id: claude-review - uses: anthropics/claude-code-action@v1 + uses: anthropics/claude-code-action@ba7fa4bcf054319261202aef93d71a89112a8d00 # v1 with: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} prompt: | diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 9f5b03fa4..872740f31 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -26,12 +26,12 @@ jobs: id-token: write steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: fetch-depth: 0 - name: Run Claude Code Review - uses: anthropics/claude-code-action@v1 + uses: anthropics/claude-code-action@ba7fa4bcf054319261202aef93d71a89112a8d00 # v1 with: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} prompt: | @@ -56,13 +56,13 @@ jobs: actions: read # Required for Claude to read CI results on PRs steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: fetch-depth: 1 - name: Run Claude Code id: claude - uses: anthropics/claude-code-action@v1 + uses: anthropics/claude-code-action@ba7fa4bcf054319261202aef93d71a89112a8d00 # v1 with: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} diff --git a/.github/workflows/publish-pro.yml b/.github/workflows/publish-pro.yml index e3c55823a..80469af44 100644 --- a/.github/workflows/publish-pro.yml +++ b/.github/workflows/publish-pro.yml @@ -18,12 +18,12 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: fetch-depth: 0 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: '20' registry-url: 'https://registry.npmjs.org' diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 7ddbf96f8..12473c59a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -24,10 +24,10 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: '20' registry-url: 'https://registry.npmjs.org' @@ -51,8 +51,11 @@ jobs: - name: Security scan — secret detection run: | - echo "Installing trufflehog..." - curl -sSfL https://raw.githubusercontent.com/trufflesecurity/trufflehog/main/scripts/install.sh | sh -s -- -b /usr/local/bin + echo "Installing trufflehog (pinned release)..." + TRUFFLEHOG_VERSION="3.88.22" + curl -sSfL "https://github.com/trufflesecurity/trufflehog/releases/download/v${TRUFFLEHOG_VERSION}/trufflehog_${TRUFFLEHOG_VERSION}_linux_amd64.tar.gz" -o trufflehog.tar.gz + tar xzf trufflehog.tar.gz -C /usr/local/bin trufflehog + rm trufflehog.tar.gz echo "Scanning repository for verified secrets..." trufflehog filesystem . --only-verified --fail --no-update 2>&1 || { diff --git a/.github/workflows/verification.yml b/.github/workflows/verification.yml index e839cb9f8..008d63e2f 100644 --- a/.github/workflows/verification.yml +++ b/.github/workflows/verification.yml @@ -16,10 +16,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: '3.11' @@ -54,10 +54,10 @@ jobs: needs: level-1-lint steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: '3.11' @@ -84,10 +84,10 @@ jobs: needs: level-2-tests steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: '3.11' @@ -132,7 +132,7 @@ jobs: needs: level-3-integrity steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Verify directory structure run: | @@ -177,7 +177,7 @@ jobs: needs: level-4-structure steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Check for hardcoded secrets run: | @@ -227,7 +227,7 @@ jobs: needs: [level-1-lint, level-2-tests, level-3-integrity, level-4-structure, level-5-security] steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Generate verification report run: |