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
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:
@@ -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 }}
|
||||
|
||||
Reference in New Issue
Block a user