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.
This commit is contained in:
Sanaei
2026-08-22 21:09:24 +02:00
parent a255ab7c65
commit a3e617215c
+34 -6
View File
@@ -494,13 +494,41 @@ jobs:
- uses: actions/checkout@v7 - uses: actions/checkout@v7
with: with:
persist-credentials: false persist-credentials: false
# Read-only: this job holds a write-scoped token, so building or running # An `@claude review` vouches for the head that existed when it was typed;
# anything out of pr-head/ would turn the review into a pwn-request. # a push after it would swap the code out from under that approval.
# checkout v7 refuses a fork PR ref outright unless that risk is accepted - name: Pin the head this run reviews
# here, and nearly every pull request to this repository is from a fork. 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 - uses: actions/checkout@v7
with: with:
ref: refs/pull/${{ github.event.pull_request.number || github.event.issue.number }}/head ref: ${{ steps.pinned-sha.outputs.sha }}
path: pr-head path: pr-head
persist-credentials: false persist-credentials: false
allow-unsafe-pr-checkout: true allow-unsafe-pr-checkout: true
@@ -534,7 +562,7 @@ jobs:
if-no-files-found: ignore if-no-files-found: ignore
retention-days: 7 retention-days: 7
- name: Fail if the review posted nothing - name: Fail if the review posted nothing
if: ${{ !cancelled() }} if: ${{ !cancelled() && steps.pinned-sha.outcome == 'success' }}
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }} REPO: ${{ github.repository }}