Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 14 additions & 10 deletions .github/workflows/review-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -207,13 +207,25 @@ jobs:
exit-code: ${{ steps.run-review.outputs.exit-code }}

steps:
# Generate GitHub App token first so the check run is created under the app's identity
# (prevents GitHub from nesting it under unrelated pull_request-triggered workflows)
- name: Generate GitHub App token
if: env.HAS_APP_SECRETS == 'true'
id: app-token
continue-on-error: true # Don't fail workflow if token generation fails
uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a # v2
with:
app_id: ${{ secrets.CAGENT_REVIEWER_APP_ID }}
private_key: ${{ secrets.CAGENT_REVIEWER_APP_PRIVATE_KEY }}

- name: Create check run
id: create-check
continue-on-error: true # Don't fail if caller didn't grant checks: write
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
env:
PR_NUMBER: ${{ inputs.pr-number || github.event.issue.number }}
with:
github-token: ${{ steps.app-token.outputs.token || github.token }}
script: |
const prNumber = parseInt(process.env.PR_NUMBER, 10);
const { data: pr } = await github.rest.pulls.get({
Expand Down Expand Up @@ -241,16 +253,6 @@ jobs:
fetch-depth: 0
ref: refs/pull/${{ github.event.issue.number }}/head

# Generate GitHub App token for custom app identity (optional - falls back to github.token)
- name: Generate GitHub App token
if: env.HAS_APP_SECRETS == 'true'
id: app-token
continue-on-error: true # Don't fail workflow if token generation fails
uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a # v2
with:
app_id: ${{ secrets.CAGENT_REVIEWER_APP_ID }}
private_key: ${{ secrets.CAGENT_REVIEWER_APP_PRIVATE_KEY }}

- name: Run PR Review
id: run-review
continue-on-error: true # Don't fail the calling workflow if the review errors
Expand All @@ -262,6 +264,7 @@ jobs:
add-prompt-files: ${{ inputs.add-prompt-files }}
model: ${{ inputs.model }}
github-token: ${{ steps.app-token.outputs.token || github.token }}
trusted-bot-app-id: ${{ secrets.CAGENT_REVIEWER_APP_ID }}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ MEDIUM: Same app ID used for both reviewer identity and authorization bypass

The workflow passes secrets.CAGENT_REVIEWER_APP_ID as the trusted-bot-app-id input. This creates a circular trust model where:

  • The app that posts reviews (reviewer identity)
  • Is also trusted to bypass authorization checks when it comments /review

While this may be intentional for legitimate automation (e.g., a trusted triage bot triggering reviews), if the app's credentials are compromised or if someone exploits the app's webhook, this bypass would allow unauthorized reviews.

Recommendation: Consider using separate apps for triggering (automation) and posting (identity), or add additional verification that the app is being used in the expected context.

anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
openai-api-key: ${{ secrets.OPENAI_API_KEY }}
google-api-key: ${{ secrets.GOOGLE_API_KEY }}
Expand All @@ -277,6 +280,7 @@ jobs:
CHECK_ID: ${{ steps.create-check.outputs.check-id }}
JOB_STATUS: ${{ job.status }}
with:
github-token: ${{ steps.app-token.outputs.token || github.token }}
script: |
const conclusion = process.env.JOB_STATUS === 'cancelled' ? 'cancelled' : process.env.JOB_STATUS === 'success' ? 'success' : 'failure';
try {
Expand Down
31 changes: 20 additions & 11 deletions .github/workflows/self-review-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,17 +106,34 @@ jobs:
# Triggers when someone comments /review on a PR
# ==========================================================================
manual-review:
if: github.event.issue.pull_request && startsWith(github.event.comment.body, '/review')
if: |
github.event.issue.pull_request &&
startsWith(github.event.comment.body, '/review') &&
(github.event.comment.user.type != 'Bot' || github.event.comment.user.login == 'docker-agent[bot]')
runs-on: ubuntu-latest
env:
HAS_APP_SECRETS: ${{ secrets.CAGENT_REVIEWER_APP_ID != '' }}

steps:
# Generate GitHub App token first so the check run is created under the app's identity
# (prevents GitHub from nesting it under unrelated pull_request-triggered workflows)
- name: Generate GitHub App token
if: env.HAS_APP_SECRETS == 'true'
id: app-token
continue-on-error: true # Don't fail workflow if token generation fails
uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a # v2
with:
app_id: ${{ secrets.CAGENT_REVIEWER_APP_ID }}
private_key: ${{ secrets.CAGENT_REVIEWER_APP_PRIVATE_KEY }}

- name: Create check run
id: create-check
continue-on-error: true # Don't fail if checks: write permission is missing
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
env:
APP_TOKEN: ${{ steps.app-token.outputs.token }}
with:
github-token: ${{ steps.app-token.outputs.token || github.token }}
script: |
const prNumber = context.issue.number;
const { data: pr } = await github.rest.pulls.get({
Expand Down Expand Up @@ -144,16 +161,6 @@ jobs:
fetch-depth: 0
ref: refs/pull/${{ github.event.issue.number }}/head

# Generate GitHub App token for custom app identity (optional - falls back to github.token)
- name: Generate GitHub App token
if: env.HAS_APP_SECRETS == 'true'
id: app-token
continue-on-error: true # Don't fail workflow if token generation fails
uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a # v2
with:
app_id: ${{ secrets.CAGENT_REVIEWER_APP_ID }}
private_key: ${{ secrets.CAGENT_REVIEWER_APP_PRIVATE_KEY }}

- name: Run PR Review
id: run-review
continue-on-error: true # Don't fail the calling workflow if the review errors
Expand All @@ -162,6 +169,7 @@ jobs:
pr-number: ${{ github.event.issue.number }}
comment-id: ${{ github.event.comment.id }}
github-token: ${{ steps.app-token.outputs.token || github.token }}
trusted-bot-app-id: ${{ secrets.CAGENT_REVIEWER_APP_ID }}
anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
openai-api-key: ${{ secrets.OPENAI_API_KEY }}
google-api-key: ${{ secrets.GOOGLE_API_KEY }}
Expand All @@ -177,6 +185,7 @@ jobs:
CHECK_ID: ${{ steps.create-check.outputs.check-id }}
JOB_STATUS: ${{ job.status }}
with:
github-token: ${{ steps.app-token.outputs.token || github.token }}
script: |
const conclusion = process.env.JOB_STATUS === 'cancelled' ? 'cancelled' : process.env.JOB_STATUS === 'success' ? 'success' : 'failure';
try {
Expand Down
24 changes: 22 additions & 2 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ inputs:
description: "Additional arguments to pass to cagent run"
required: false
default: ""
trusted-bot-app-id:
description: "GitHub App ID of a trusted bot that can bypass comment-based auth checks (e.g., for self-review triggers)"
required: false
default: ""
add-prompt-files:
description: "Comma-separated list of files to append to the prompt (e.g., 'AGENTS.md,CLAUDE.md')"
required: false
Expand Down Expand Up @@ -190,10 +194,12 @@ runs:
shell: bash
env:
ACTION_PATH: ${{ github.action_path }}
# Get author_association from comment events (the main risk)
COMMENT_ASSOCIATION: ${{ github.event.comment.author_association }}
TRUSTED_BOT_APP_ID: ${{ inputs.trusted-bot-app-id }}
DEBUG: ${{ inputs.debug }}
run: |
# Read comment fields directly from the event payload (cannot be overridden by workflow env vars)
COMMENT_ASSOCIATION=$(jq -r '.comment.author_association // empty' "$GITHUB_EVENT_PATH")

# Only enforce auth for comment-triggered events
# This prevents abuse via /commands while allowing PR-triggered workflows to run
if [ -z "$COMMENT_ASSOCIATION" ]; then
Expand All @@ -202,6 +208,20 @@ runs:
exit 0
fi

# Allow a trusted GitHub App bot to bypass auth (e.g., auto-triage posts /review).
# Verified via user type + app ID from the event payload to prevent spoofing.
if [ -n "$TRUSTED_BOT_APP_ID" ]; then
COMMENT_USER_TYPE=$(jq -r '.comment.user.type // empty' "$GITHUB_EVENT_PATH")
COMMENT_APP_ID=$(jq -r '.comment.performed_via_github_app.id // empty' "$GITHUB_EVENT_PATH")

if [ "$COMMENT_USER_TYPE" = "Bot" ] && [ -n "$COMMENT_APP_ID" ] && [ "$COMMENT_APP_ID" = "$TRUSTED_BOT_APP_ID" ]; then
COMMENT_USER_LOGIN=$(jq -r '.comment.user.login // empty' "$GITHUB_EVENT_PATH")
echo "ℹ️ Skipping auth check (trusted bot: $COMMENT_USER_LOGIN, app_id: $COMMENT_APP_ID)"
echo "authorized=true" >> $GITHUB_OUTPUT
exit 0
fi
fi

echo "Using comment author_association: $COMMENT_ASSOCIATION"

# Allowed roles (hardcoded for security - cannot be overridden)
Expand Down
6 changes: 6 additions & 0 deletions review-pr/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ inputs:
description: "Comma-separated list of files to append to the prompt (e.g., 'AGENTS.md,CLAUDE.md')"
required: false
default: ""
trusted-bot-app-id:
description: "GitHub App ID of a trusted bot that can bypass comment-based auth checks"
required: false
default: ""

outputs:
exit-code:
Expand Down Expand Up @@ -473,6 +477,7 @@ runs:
nebius-api-key: ${{ inputs.nebius-api-key }}
mistral-api-key: ${{ inputs.mistral-api-key }}
github-token: ${{ steps.resolve-token.outputs.token }}
trusted-bot-app-id: ${{ inputs.trusted-bot-app-id }}
extra-args: ${{ inputs.model && format('--model={0}', inputs.model) || '' }}

# ========================================
Expand Down Expand Up @@ -560,6 +565,7 @@ runs:
nebius-api-key: ${{ inputs.nebius-api-key }}
mistral-api-key: ${{ inputs.mistral-api-key }}
github-token: ${{ steps.resolve-token.outputs.token }}
trusted-bot-app-id: ${{ inputs.trusted-bot-app-id }}
extra-args: ${{ inputs.model && format('--model={0}', inputs.model) || '' }}
add-prompt-files: ${{ inputs.add-prompt-files }}
max-retries: "0" # Disable retries — the review agent recovers internally (root falls back when sub-agents fail), so retrying the pipeline produces duplicate reviews
Expand Down
Loading