Explorar o código

fix(ci): pin the head the review job checks out

The review job checked the pull request out through refs/pull/N/head, a
ref the author can move after a maintainer types "@claude review". Code
scanning flagged it twice on the issue_comment path: an untrusted
checkout in a privileged context (alert 111) and the time-of-check /
time-of-use race that ref creates (alert 110).

Resolve the head once, up front, and refuse the run when the fork was
pushed to after the request that vouched for it, mirroring the freshness
gate resolve-conflicts already uses; the checkout then names that
immutable SHA. pull_request_target runs take the head SHA straight from
the payload, so they skip the comparison. The trailing "posted nothing"
check no longer fires on top of a refusal, which would otherwise report
a second, misleading failure.
Sanaei hai 8 horas
pai
achega
a3e617215c
Modificáronse 1 ficheiros con 34 adicións e 6 borrados
  1. 34 6
      .github/workflows/claude-bot.yml

+ 34 - 6
.github/workflows/claude-bot.yml

@@ -494,13 +494,41 @@ jobs:
       - uses: actions/checkout@v7
         with:
           persist-credentials: false
-      # Read-only: this job holds a write-scoped token, so building or running
-      # anything out of pr-head/ would turn the review into a pwn-request.
-      # checkout v7 refuses a fork PR ref outright unless that risk is accepted
-      # here, and nearly every pull request to this repository is from a fork.
+      # An `@claude review` vouches for the head that existed when it was typed;
+      # a push after it would swap the code out from under that approval.
+      - name: Pin the head this run reviews
+        id: pinned-sha
+        env:
+          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          REPO: ${{ github.repository }}
+          PR: ${{ github.event.pull_request.number || github.event.issue.number }}
+          PAYLOAD_SHA: ${{ github.event.pull_request.head.sha }}
+          COMMENT_AT: ${{ github.event.comment.created_at }}
+        run: |
+          set -euo pipefail
+          if [ -n "$PAYLOAD_SHA" ]; then
+            echo "sha=${PAYLOAD_SHA}" >> "$GITHUB_OUTPUT"
+            exit 0
+          fi
+          head=$(gh api "repos/${REPO}/pulls/${PR}" --jq '"\(.head.sha) \(.head.repo.pushed_at // "")"')
+          HEAD_SHA=${head%% *}
+          HEAD_PUSHED_AT=${head#* }
+          if [ -z "$HEAD_PUSHED_AT" ]; then
+            gh pr comment "$PR" --repo "$REPO" --body "The head repository of this pull request is gone, so the code to review cannot be verified. Nothing was reviewed."
+            echo "::error::The head repository is unavailable; refusing to check it out."
+            exit 1
+          fi
+          if [ "$(date -d "$HEAD_PUSHED_AT" +%s)" -gt "$(date -d "$COMMENT_AT" +%s)" ]; then
+            gh pr comment "$PR" --repo "$REPO" --body "The head branch was pushed to at ${HEAD_PUSHED_AT}, after this review was requested at ${COMMENT_AT}, so the code that would be checked out here is not the code the request vouched for. Nothing was reviewed. Ask again to review the current head."
+            echo "::error::The head moved after the request; refusing to check it out."
+            exit 1
+          fi
+          echo "sha=${HEAD_SHA}" >> "$GITHUB_OUTPUT"
+      # Read-only, and pinned to one immutable commit: this job holds a
+      # write-scoped token, so running anything out of pr-head/ would be a pwn-request.
       - uses: actions/checkout@v7
         with:
-          ref: refs/pull/${{ github.event.pull_request.number || github.event.issue.number }}/head
+          ref: ${{ steps.pinned-sha.outputs.sha }}
           path: pr-head
           persist-credentials: false
           allow-unsafe-pr-checkout: true
@@ -534,7 +562,7 @@ jobs:
           if-no-files-found: ignore
           retention-days: 7
       - name: Fail if the review posted nothing
-        if: ${{ !cancelled() }}
+        if: ${{ !cancelled() && steps.pinned-sha.outcome == 'success' }}
         env:
           GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
           REPO: ${{ github.repository }}