fix(action): address security review - prevent token exfiltration and add input validation
PR Ready Gate / clear-labels (pull_request) Successful in 2s
CI / test (pull_request) Successful in 22s
CI / review (anthropic--claude-4.6-sonnet, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Successful in 33s
CI / review (gpt-5, security, ., rodin/security-patterns, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Successful in 1m14s
CI / review (gpt-5, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 1m58s

Security fixes:
- On GitHub/GHES (VCS_TYPE=github), inputs.gitea-url is now completely
  ignored. API calls use github.api_url; downloads use github.server_url.
  Tokens are never sent to user-supplied URLs.
- Replace action_token step output with masked GITHUB_ENV variable to
  prevent token leakage in debug logs.
- Validate action-repo against owner/repo pattern to prevent path traversal.
- Validate SERVER_URL in Gitea path: require https:// scheme, reject
  whitespace and newlines.
- Strengthen VERSION validation: block slashes and whitespace in addition
  to newlines.
- Add integrity check in Install step: verify SERVER_URL matches
  github.server_url on GitHub runners.

Addresses findings from security-review-bot on PR #121.
Deferred: full IP-level SSRF defense (see #123).
This commit is contained in:
claw
2026-05-13 21:29:15 -07:00
parent 646497de68
commit 93d89ba662
+70 -19
View File
@@ -2,13 +2,23 @@
# 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.
#
# Security notes:
# - On GitHub/GHES (VCS_TYPE=github), inputs.gitea-url is IGNORED to prevent
# token exfiltration. API calls use github.api_url; downloads use
# github.server_url. Tokens are never sent to user-supplied URLs.
# - On Gitea (VCS_TYPE=gitea), inputs.gitea-url is validated (https scheme,
# no whitespace/newlines) before use.
# - action-repo is validated against owner/repo pattern.
# - Tokens are passed via masked environment variables, not step outputs.
#
# Requirements: python3, sha256sum, curl (all present on ubuntu-* runners).
name: 'AI Code Review'
description: 'Run AI-powered code review on a pull request using review-bot'
inputs:
gitea-url:
description: 'VCS instance URL (defaults to server_url). Works for both Gitea and GitHub.'
description: 'Gitea instance URL (only used on Gitea runners; ignored on GitHub/GHES). Defaults to server_url.'
required: false
default: ''
repo:
@@ -121,9 +131,7 @@ runs:
id: version
shell: bash
run: |
SERVER_URL="${{ inputs.gitea-url || github.server_url }}"
# Strip trailing slash if present
SERVER_URL="${SERVER_URL%/}"
# --- Input Validation ---
# Determine the repo hosting review-bot releases (not the repo being reviewed)
ACTION_REPO="${{ inputs.action-repo }}"
@@ -136,6 +144,12 @@ runs:
ACTION_REPO="rodin/review-bot"
fi
# Validate ACTION_REPO matches owner/repo pattern (防止 path traversal)
if ! printf '%s' "$ACTION_REPO" | grep -qE '^[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+$'; then
echo "Error: action-repo '${ACTION_REPO}' does not match expected owner/repo format" >&2
exit 1
fi
# Detect VCS host type using github.api_url context.
# github.api_url is set on GitHub.com (https://api.github.com) and GHES
# (https://<host>/api/v3). It is empty/unset on Gitea Actions runners.
@@ -146,6 +160,32 @@ runs:
VCS_TYPE="gitea"
fi
# Determine SERVER_URL based on VCS type.
# SECURITY: On GitHub/GHES, ALWAYS use github.server_url — never trust
# inputs.gitea-url to prevent token exfiltration to attacker-controlled hosts.
if [ "$VCS_TYPE" = "github" ]; then
SERVER_URL="${{ github.server_url }}"
if [ -n "${{ inputs.gitea-url }}" ]; then
echo "::warning::inputs.gitea-url is ignored on GitHub/GHES runners (VCS_TYPE=github). Using github.server_url instead."
fi
else
SERVER_URL="${{ inputs.gitea-url || github.server_url }}"
fi
# Strip trailing slash if present
SERVER_URL="${SERVER_URL%/}"
# Validate SERVER_URL for Gitea path: must be https, no whitespace/newlines
if [ "$VCS_TYPE" = "gitea" ]; then
if ! printf '%s' "$SERVER_URL" | grep -qE '^https://[^[:space:]]+$'; then
echo "Error: SERVER_URL '${SERVER_URL}' must be an https:// URL with no whitespace" >&2
exit 1
fi
if printf '%s' "$SERVER_URL" | grep -q $'\n'; then
echo "Error: SERVER_URL contains unexpected newline" >&2
exit 1
fi
fi
# Determine auth token for release API requests
ACTION_TOKEN="${{ inputs.action-repo-token }}"
if [ -z "$ACTION_TOKEN" ]; then
@@ -158,11 +198,11 @@ runs:
if [ "${{ inputs.version }}" = "latest" ]; then
if [ "$VCS_TYPE" = "github" ]; then
# Use github.api_url which resolves correctly for both github.com
# (https://api.github.com) and GHES (https://<host>/api/v3)
# SECURITY: Use github.api_url which is a trusted platform-provided value.
# Never construct API URLs from user-supplied inputs on GitHub.
API_URL="${GITHUB_API_URL}/repos/${ACTION_REPO}/releases?per_page=1"
else
# Gitea API
# Gitea API — SERVER_URL was validated above
API_URL="${SERVER_URL}/api/v1/repos/${ACTION_REPO}/releases?limit=1"
fi
@@ -193,9 +233,9 @@ runs:
VERSION="${{ inputs.version }}"
fi
# Validate outputs don't contain newlines (defensive against injection)
if printf '%s' "$VERSION" | grep -q $'\n'; then
echo "Error: VERSION contains unexpected newline" >&2
# Validate VERSION: no newlines, no slashes, no whitespace (防止 path traversal)
if printf '%s' "$VERSION" | grep -qE '[\n\r/[:space:]]'; then
echo "Error: VERSION '${VERSION}' contains invalid characters (newline, slash, or whitespace)" >&2
exit 1
fi
@@ -203,7 +243,13 @@ runs:
echo "action_repo=${ACTION_REPO}" >> "$GITHUB_OUTPUT"
echo "server_url=${SERVER_URL}" >> "$GITHUB_OUTPUT"
echo "vcs_type=${VCS_TYPE}" >> "$GITHUB_OUTPUT"
echo "action_token=${ACTION_TOKEN}" >> "$GITHUB_OUTPUT"
# SECURITY: Pass token via masked environment variable instead of step output.
# Step outputs can leak in debug logs; GITHUB_ENV with masking is safer.
if [ -n "$ACTION_TOKEN" ]; then
echo "::add-mask::${ACTION_TOKEN}"
echo "ACTION_TOKEN=${ACTION_TOKEN}" >> "$GITHUB_ENV"
fi
- name: Cache review-bot binary
id: cache
@@ -220,16 +266,21 @@ runs:
ACTION_REPO="${{ steps.version.outputs.action_repo }}"
VERSION="${{ steps.version.outputs.version }}"
VCS_TYPE="${{ steps.version.outputs.vcs_type }}"
ACTION_TOKEN="${{ steps.version.outputs.action_token }}"
# Read token from masked environment variable (set in Determine version step)
# Falls back to empty if not set (public repos don't need auth)
ACTION_TOKEN="${ACTION_TOKEN:-}"
BINARY="review-bot-linux-amd64"
# Build auth args if token is available (supports private repos)
AUTH_ARGS=""
if [ -n "$ACTION_TOKEN" ]; then
if [ "$VCS_TYPE" = "github" ]; then
AUTH_ARGS="-H \"Authorization: Bearer ${ACTION_TOKEN}\""
else
AUTH_ARGS="-H \"Authorization: token ${ACTION_TOKEN}\""
# 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
exit 1
fi
fi