From e709956d0b277fbc028e876ad8a291cf19b2f33f Mon Sep 17 00:00:00 2001 From: claw Date: Wed, 13 May 2026 21:38:49 -0700 Subject: [PATCH] fix(action): use REST API for GitHub asset downloads, enforce trusted GITEA_URL 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). --- .gitea/actions/review/action.yml | 97 ++++++++++++++++++++++++-------- 1 file changed, 73 insertions(+), 24 deletions(-) diff --git a/.gitea/actions/review/action.yml b/.gitea/actions/review/action.yml index f249410..4e275da 100644 --- a/.gitea/actions/review/action.yml +++ b/.gitea/actions/review/action.yml @@ -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 }}