From eb4976a090fb5e134f5cd6d151470d7aa6c80078 Mon Sep 17 00:00:00 2001 From: Andrew Goldis Date: Sun, 16 Aug 2026 11:51:30 -0700 Subject: [PATCH 1/2] ci: fix shell injection in dependabot-review workflow The PR title was interpolated into a run: block as PR_TITLE="${{ github.event.pull_request.title }}", so a title containing $(...) or backticks ran as shell on the runner. The if: gate keyed on the branch-name prefix, which any fork can set, so an untrusted PR reached it. Impact was limited because the pull_request trigger gives fork PRs a read-only token and no secrets, but the pattern becomes a credential leak the moment the trigger or permissions change. Pass PR fields through env and reference them as shell variables, and gate on github.actor == 'dependabot[bot]' so only real Dependabot PRs run. Co-Authored-By: Claude Fable 5 --- .github/workflows/dependabot-review.yml | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/.github/workflows/dependabot-review.yml b/.github/workflows/dependabot-review.yml index 8d95cf4..9e39a57 100644 --- a/.github/workflows/dependabot-review.yml +++ b/.github/workflows/dependabot-review.yml @@ -7,7 +7,9 @@ on: jobs: cursor-agent-review: name: Cursor Agent Dependency Review - if: startsWith(github.head_ref, 'dependabot/') + # Gate on the actor, not the branch name: any fork can name a branch + # dependabot/... but only real Dependabot PRs run as dependabot[bot]. + if: github.actor == 'dependabot[bot]' && startsWith(github.head_ref, 'dependabot/') runs-on: ubuntu-latest timeout-minutes: 35 permissions: @@ -18,10 +20,11 @@ jobs: id: launch env: CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }} + # Pass PR fields through env, never inline ${{ }} in run: the title is + # attacker-controlled and would otherwise be executed as shell. + PR_URL: ${{ github.event.pull_request.html_url }} + PR_TITLE: ${{ github.event.pull_request.title }} run: | - PR_URL="${{ github.event.pull_request.html_url }}" - PR_TITLE="${{ github.event.pull_request.title }}" - PROMPT=$(cat <<'PROMPT_EOF' You are reviewing a Dependabot pull request for the currents-mcp repository. This is a TypeScript MCP server located in the `mcp-server/` directory. @@ -197,13 +200,14 @@ jobs: env: CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }} SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + # Pass interpolated values through env, never inline ${{ }} in run: + # PR_TITLE is attacker-controlled; the rest follow the same rule. + AGENT_ID: ${{ steps.launch.outputs.agent_id }} + AGENT_URL: ${{ steps.launch.outputs.agent_url }} + AGENT_STATUS: ${{ steps.poll.outputs.status }} + PR_URL: ${{ github.event.pull_request.html_url }} + PR_TITLE: ${{ github.event.pull_request.title }} run: | - AGENT_ID="${{ steps.launch.outputs.agent_id }}" - AGENT_URL="${{ steps.launch.outputs.agent_url }}" - AGENT_STATUS="${{ steps.poll.outputs.status }}" - PR_URL="${{ github.event.pull_request.html_url }}" - PR_TITLE="${{ github.event.pull_request.title }}" - SUMMARY="" if [ -n "$AGENT_ID" ] && [ "$AGENT_ID" != "null" ]; then SUMMARY=$(curl -s -u "$CURSOR_API_KEY:" \ From 2f48981c6c098b677ddfb55c6f3c6b14864c8512 Mon Sep 17 00:00:00 2001 From: Andrew Goldis Date: Sun, 16 Aug 2026 17:56:01 -0700 Subject: [PATCH 2/2] ci: gate dependabot review on PR author, not triggering actor github.actor is whoever triggered the event, so a maintainer reopening or pushing to a real Dependabot PR would flip it and skip the review and Slack notification. pull_request.user.login is the PR author, stable across who triggers, and still blocks the fork bypass since an attacker's PR is authored by their own account, not dependabot[bot]. Co-Authored-By: Claude Fable 5 --- .github/workflows/dependabot-review.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/dependabot-review.yml b/.github/workflows/dependabot-review.yml index 9e39a57..70d9cec 100644 --- a/.github/workflows/dependabot-review.yml +++ b/.github/workflows/dependabot-review.yml @@ -7,9 +7,11 @@ on: jobs: cursor-agent-review: name: Cursor Agent Dependency Review - # Gate on the actor, not the branch name: any fork can name a branch - # dependabot/... but only real Dependabot PRs run as dependabot[bot]. - if: github.actor == 'dependabot[bot]' && startsWith(github.head_ref, 'dependabot/') + # Gate on the PR author, not the branch name: any fork can name a branch + # dependabot/... but only real Dependabot PRs are authored by dependabot[bot]. + # Use pull_request.user.login rather than github.actor so a maintainer who + # reopens or pushes to the PR does not flip the actor and skip the review. + if: github.event.pull_request.user.login == 'dependabot[bot]' && startsWith(github.head_ref, 'dependabot/') runs-on: ubuntu-latest timeout-minutes: 35 permissions: