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.
|
# 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
|
# 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
|
# 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:
|
# Security notes:
|
||||||
# - On GitHub/GHES (VCS_TYPE=github), inputs.gitea-url is IGNORED to prevent
|
# - On GitHub/GHES (VCS_TYPE=github), inputs.gitea-url is IGNORED to prevent
|
||||||
@@ -271,44 +271,93 @@ runs:
|
|||||||
ACTION_TOKEN="${ACTION_TOKEN:-}"
|
ACTION_TOKEN="${ACTION_TOKEN:-}"
|
||||||
BINARY="review-bot-linux-amd64"
|
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
|
if [ "$VCS_TYPE" = "github" ]; then
|
||||||
# Double-check: SERVER_URL must be github.server_url (platform-provided)
|
# GitHub/GHES: Use REST API for release asset downloads.
|
||||||
EXPECTED_SERVER="${{ github.server_url }}"
|
# Web release URLs ({server}/.../releases/download/{tag}/{asset}) redirect
|
||||||
EXPECTED_SERVER="${EXPECTED_SERVER%/}"
|
# to S3 and don't reliably support Authorization headers for private repos.
|
||||||
if [ "$SERVER_URL" != "$EXPECTED_SERVER" ]; then
|
# The REST API endpoint with Accept: application/octet-stream is required.
|
||||||
echo "Error: SERVER_URL mismatch on GitHub runner (possible tampering)" >&2
|
GITHUB_API_URL="${{ github.api_url }}"
|
||||||
exit 1
|
|
||||||
fi
|
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
|
fi
|
||||||
|
|
||||||
# Download URL format is the same on both Gitea and GitHub:
|
# Extract asset IDs for binary and checksums
|
||||||
# {server}/{owner}/{repo}/releases/download/{tag}/{asset}
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# 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}" \
|
||||||
|
-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}"
|
DOWNLOAD_URL="${SERVER_URL}/${ACTION_REPO}/releases/download/${VERSION}"
|
||||||
|
|
||||||
if [ -n "$ACTION_TOKEN" ]; then
|
if [ -n "$ACTION_TOKEN" ]; then
|
||||||
if [ "$VCS_TYPE" = "github" ]; then
|
|
||||||
curl -sSfL --connect-timeout 10 --max-time 120 \
|
curl -sSfL --connect-timeout 10 --max-time 120 \
|
||||||
-H "Authorization: Bearer ${ACTION_TOKEN}" \
|
-H "Authorization: token ${ACTION_TOKEN}" \
|
||||||
"${DOWNLOAD_URL}/${BINARY}" -o "${{ runner.temp }}/review-bot"
|
"${DOWNLOAD_URL}/${BINARY}" -o "${{ runner.temp }}/review-bot"
|
||||||
curl -sSfL --connect-timeout 10 --max-time 30 \
|
curl -sSfL --connect-timeout 10 --max-time 30 \
|
||||||
-H "Authorization: Bearer ${ACTION_TOKEN}" \
|
-H "Authorization: token ${ACTION_TOKEN}" \
|
||||||
"${DOWNLOAD_URL}/checksums.txt" -o "${{ runner.temp }}/checksums.txt"
|
"${DOWNLOAD_URL}/checksums.txt" -o "${{ runner.temp }}/checksums.txt"
|
||||||
else
|
else
|
||||||
curl -sSfL --connect-timeout 10 --max-time 120 \
|
curl -sSfL --connect-timeout 10 --max-time 120 \
|
||||||
-H "Authorization: token ${ACTION_TOKEN}" \
|
|
||||||
"${DOWNLOAD_URL}/${BINARY}" -o "${{ runner.temp }}/review-bot"
|
"${DOWNLOAD_URL}/${BINARY}" -o "${{ runner.temp }}/review-bot"
|
||||||
curl -sSfL --connect-timeout 10 --max-time 30 \
|
curl -sSfL --connect-timeout 10 --max-time 30 \
|
||||||
-H "Authorization: token ${ACTION_TOKEN}" \
|
|
||||||
"${DOWNLOAD_URL}/checksums.txt" -o "${{ runner.temp }}/checksums.txt"
|
"${DOWNLOAD_URL}/checksums.txt" -o "${{ runner.temp }}/checksums.txt"
|
||||||
fi
|
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
|
fi
|
||||||
|
|
||||||
# Verify SHA-256 checksum
|
# Verify SHA-256 checksum
|
||||||
@@ -336,7 +385,7 @@ runs:
|
|||||||
- name: Run review
|
- name: Run review
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
GITEA_URL: ${{ inputs.gitea-url || github.server_url }}
|
GITEA_URL: ${{ steps.version.outputs.server_url }}
|
||||||
GITEA_REPO: ${{ inputs.repo || github.repository }}
|
GITEA_REPO: ${{ inputs.repo || github.repository }}
|
||||||
PR_NUMBER: ${{ inputs.pr-number || github.event.pull_request.number }}
|
PR_NUMBER: ${{ inputs.pr-number || github.event.pull_request.number }}
|
||||||
REVIEWER_TOKEN: ${{ inputs.reviewer-token }}
|
REVIEWER_TOKEN: ${{ inputs.reviewer-token }}
|
||||||
|
|||||||
Reference in New Issue
Block a user