fix(action): use REST API for GitHub asset downloads, enforce trusted GITEA_URL
PR Ready Gate / clear-labels (pull_request) Successful in 1s
CI / test (pull_request) Successful in 18s
CI / review (anthropic--claude-4.6-sonnet, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Successful in 34s
CI / review (gpt-5, security, ., rodin/security-patterns, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Successful in 1m21s
CI / review (gpt-5, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 1m42s

Addresses gpt-review-bot findings on PR #121:

MAJOR #1: The 'Run review' step set GITEA_URL from inputs.gitea-url which
could exfiltrate the reviewer token to an attacker-controlled host on
GitHub/GHES. Now uses steps.version.outputs.server_url which enforces
VCS-type-aware trust (github.server_url on GitHub, validated input on Gitea).

MAJOR #2: Private release asset downloads on GitHub/GHES used web URLs
({server}/.../releases/download/{tag}/{asset}) which redirect to S3 and
don't support Authorization headers for private repos. Now uses the GitHub
REST API: fetches release metadata by tag, extracts asset IDs, and downloads
via /repos/{owner}/{repo}/releases/assets/{id} with Accept: octet-stream.
Gitea path retains direct URL downloads (which work correctly there).
This commit is contained in:
claw
2026-05-13 21:38:49 -07:00
parent 93d89ba662
commit e709956d0b
+73 -24
View File
@@ -1,7 +1,7 @@
# This composite action supports both Gitea Actions and GitHub Actions runners.
# It detects the VCS host type using the github.api_url context (set only on
# GitHub/GHES runners) and uses the appropriate releases API for version
# resolution and binary download.
# resolution and binary download (REST API on GitHub, direct URLs on Gitea).
#
# Security notes:
# - On GitHub/GHES (VCS_TYPE=github), inputs.gitea-url is IGNORED to prevent
@@ -271,44 +271,93 @@ runs:
ACTION_TOKEN="${ACTION_TOKEN:-}"
BINARY="review-bot-linux-amd64"
# SECURITY: On GitHub/GHES, use github.server_url for downloads.
# SERVER_URL is already set correctly per VCS_TYPE in Determine version step,
# but verify the download destination matches expectations.
if [ "$VCS_TYPE" = "github" ]; then
# Double-check: SERVER_URL must be github.server_url (platform-provided)
EXPECTED_SERVER="${{ github.server_url }}"
EXPECTED_SERVER="${EXPECTED_SERVER%/}"
if [ "$SERVER_URL" != "$EXPECTED_SERVER" ]; then
echo "Error: SERVER_URL mismatch on GitHub runner (possible tampering)" >&2
# GitHub/GHES: Use REST API for release asset downloads.
# Web release URLs ({server}/.../releases/download/{tag}/{asset}) redirect
# to S3 and don't reliably support Authorization headers for private repos.
# The REST API endpoint with Accept: application/octet-stream is required.
GITHUB_API_URL="${{ github.api_url }}"
if [ -n "$ACTION_TOKEN" ]; then
RELEASE_JSON=$(curl -sSf --connect-timeout 10 --max-time 30 \
-H "Authorization: Bearer ${ACTION_TOKEN}" \
"${GITHUB_API_URL}/repos/${ACTION_REPO}/releases/tags/${VERSION}")
else
RELEASE_JSON=$(curl -sSf --connect-timeout 10 --max-time 30 \
"${GITHUB_API_URL}/repos/${ACTION_REPO}/releases/tags/${VERSION}")
fi
# Extract asset IDs for binary and checksums
BINARY_ASSET_ID=$(printf '%s' "$RELEASE_JSON" | python3 -c "
import sys, json
assets = json.load(sys.stdin).get('assets', [])
for a in assets:
if a['name'] == '${BINARY}':
print(a['id'])
break
else:
sys.exit(1)
")
if [ -z "$BINARY_ASSET_ID" ]; then
echo "Error: could not find asset '${BINARY}' in release ${VERSION}" >&2
exit 1
fi
fi
# Download URL format is the same on both Gitea and GitHub:
# {server}/{owner}/{repo}/releases/download/{tag}/{asset}
DOWNLOAD_URL="${SERVER_URL}/${ACTION_REPO}/releases/download/${VERSION}"
CHECKSUMS_ASSET_ID=$(printf '%s' "$RELEASE_JSON" | python3 -c "
import sys, json
assets = json.load(sys.stdin).get('assets', [])
for a in assets:
if a['name'] == 'checksums.txt':
print(a['id'])
break
else:
sys.exit(1)
")
if [ -z "$CHECKSUMS_ASSET_ID" ]; then
echo "Error: could not find asset 'checksums.txt' in release ${VERSION}" >&2
exit 1
fi
if [ -n "$ACTION_TOKEN" ]; then
if [ "$VCS_TYPE" = "github" ]; then
# Download assets via REST API with Accept: application/octet-stream
if [ -n "$ACTION_TOKEN" ]; then
curl -sSfL --connect-timeout 10 --max-time 120 \
-H "Authorization: Bearer ${ACTION_TOKEN}" \
"${DOWNLOAD_URL}/${BINARY}" -o "${{ runner.temp }}/review-bot"
-H "Accept: application/octet-stream" \
"${GITHUB_API_URL}/repos/${ACTION_REPO}/releases/assets/${BINARY_ASSET_ID}" \
-o "${{ runner.temp }}/review-bot"
curl -sSfL --connect-timeout 10 --max-time 30 \
-H "Authorization: Bearer ${ACTION_TOKEN}" \
-H "Accept: application/octet-stream" \
"${GITHUB_API_URL}/repos/${ACTION_REPO}/releases/assets/${CHECKSUMS_ASSET_ID}" \
-o "${{ runner.temp }}/checksums.txt"
else
curl -sSfL --connect-timeout 10 --max-time 120 \
-H "Accept: application/octet-stream" \
"${GITHUB_API_URL}/repos/${ACTION_REPO}/releases/assets/${BINARY_ASSET_ID}" \
-o "${{ runner.temp }}/review-bot"
curl -sSfL --connect-timeout 10 --max-time 30 \
-H "Accept: application/octet-stream" \
"${GITHUB_API_URL}/repos/${ACTION_REPO}/releases/assets/${CHECKSUMS_ASSET_ID}" \
-o "${{ runner.temp }}/checksums.txt"
fi
else
# Gitea: Direct download via web release URLs (Gitea serves assets
# directly and supports token auth on these URLs)
DOWNLOAD_URL="${SERVER_URL}/${ACTION_REPO}/releases/download/${VERSION}"
if [ -n "$ACTION_TOKEN" ]; then
curl -sSfL --connect-timeout 10 --max-time 120 \
-H "Authorization: token ${ACTION_TOKEN}" \
"${DOWNLOAD_URL}/${BINARY}" -o "${{ runner.temp }}/review-bot"
curl -sSfL --connect-timeout 10 --max-time 30 \
-H "Authorization: token ${ACTION_TOKEN}" \
"${DOWNLOAD_URL}/checksums.txt" -o "${{ runner.temp }}/checksums.txt"
else
curl -sSfL --connect-timeout 10 --max-time 120 \
-H "Authorization: token ${ACTION_TOKEN}" \
"${DOWNLOAD_URL}/${BINARY}" -o "${{ runner.temp }}/review-bot"
curl -sSfL --connect-timeout 10 --max-time 30 \
-H "Authorization: token ${ACTION_TOKEN}" \
"${DOWNLOAD_URL}/checksums.txt" -o "${{ runner.temp }}/checksums.txt"
fi
else
curl -sSfL --connect-timeout 10 --max-time 120 \
"${DOWNLOAD_URL}/${BINARY}" -o "${{ runner.temp }}/review-bot"
curl -sSfL --connect-timeout 10 --max-time 30 \
"${DOWNLOAD_URL}/checksums.txt" -o "${{ runner.temp }}/checksums.txt"
fi
# Verify SHA-256 checksum
@@ -336,7 +385,7 @@ runs:
- name: Run review
shell: bash
env:
GITEA_URL: ${{ inputs.gitea-url || github.server_url }}
GITEA_URL: ${{ steps.version.outputs.server_url }}
GITEA_REPO: ${{ inputs.repo || github.repository }}
PR_NUMBER: ${{ inputs.pr-number || github.event.pull_request.number }}
REVIEWER_TOKEN: ${{ inputs.reviewer-token }}