Skip to content

Commit 2abf017

Browse files
authored
fix(ci): dicht ongeauthenticeerde @claude-trigger (RCE op publieke repo) (#25)
* fix(ci): vereis vertrouwde repo-rol voor Claude-trigger claude.yml draaide een agent met Bash + een schrijfrechten-token zodra een issue/comment de tekst "@claude" bevatte, zonder controle op wie de auteur is. Op deze publieke repo kon daardoor elke GitHub-gebruiker via een enkele comment code laten uitvoeren in een runner met het GITHUB_TOKEN en het CLAUDE_CODE_OAUTH_TOKEN. De job draait nu alleen wanneer de auteur OWNER, MEMBER of COLLABORATOR is, voor alle vier de triggers (issue_comment, pull_request_review_comment, pull_request_review, issues). claude-code-review.yml is defensief gehard: pull_request-velden lopen nu via env-variabelen in plaats van directe ${{ }}-interpolatie in het Bash-blok, zodat script-injectie via PR-metadata wordt voorkomen. * fix(ci): pin claude-code-action naar commit-SHA @v1 is een mutable tag: GitHub resolvet die bij elke run naar waar v1 op dat moment naar wijst. Een gecompromitteerde upstream-tag zou daarmee direct in de drie workflows landen die de agent met een write-scoped token draaien (claude, claude-code-review, api-sync). Alle drie nu gepind op het commit-SHA waar v1 nu naar wijst (51ea8ea), met '# v1' als leesbare annotatie. Conform de hardening- richtlijn van anthropics/claude-code-action zelf. * fix(ci): verklein aanvalsoppervlak Claude-workflows verder Twee aanvullende hardeningen bovenop de @claude-auth-gate: 1. id-token: write verwijderd uit claude, claude-code-review en api-sync. Geen enkele step in deze workflows consumeert een OIDC-token (geen configure-aws-credentials o.i.d.), dus de permissie was dood gewicht. Met id-token: write kon een job een OIDC-token munten met de identiteit van deze repo; relevant voor claude-code-review omdat die op elke fork-PR draait. 2. claude-code-review draait nu alleen op same-repo PRs (head.repo.fork == false). Die workflow checkt de PR-head uit en de review-prompt leest bestanden uit die tree (CLAUDE.md etc). Op een fork-PR is dat door een aanvaller bestuurde code, met CLAUDE_CODE_OAUTH_TOKEN in de omgeving en Bash aan: een klassieke prompt-injection-route. De env-var-hardening dekte alleen ${{ }}-injectie, niet content-injectie uit de checkout. api-sync zelf blijft ongewijzigd qua logica: die heeft al een sanitize-step, gepinde oasdiff en draait niet op untrusted triggers. * fix(ci): herstel id-token: write voor claude-code-action Vorige commit verwijderde id-token: write in de aanname dat geen enkele step het OIDC-token consumeert. Dat was fout: claude-code-action gebruikt het token intern om te authenticeren tegen de Anthropic GitHub App. Zonder de permissie faalt elke run met: Failed to get OIDC token: Unable to get ACTIONS_ID_TOKEN_REQUEST_URL env variable id-token: write is hersteld op de drie jobs die de action draaien (claude, claude-review, api-sync/implement). De daadwerkelijke hardening tegen ongeauthenticeerd misbruik blijft de auth-gate (claude.yml) en de fork-gate (claude-code-review.yml): die zorgen dat het token niet vanuit een untrusted trigger bereikbaar is. De permissie zelf weghalen was geen mitigatie maar een storing.
1 parent a478f2a commit 2abf017

3 files changed

Lines changed: 47 additions & 15 deletions

File tree

.github/workflows/api-sync.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,10 @@ jobs:
180180
contents: write
181181
pull-requests: write
182182
issues: write
183+
# claude-code-action mints an OIDC token to authenticate to the
184+
# Anthropic GitHub App. Required by the action itself. Safe here
185+
# because api-sync only runs on schedule / workflow_dispatch, never
186+
# on an untrusted trigger.
183187
id-token: write
184188

185189
steps:
@@ -231,7 +235,7 @@ jobs:
231235
} >> "$GITHUB_OUTPUT"
232236
233237
- name: Implement with Claude
234-
uses: anthropics/claude-code-action@v1
238+
uses: anthropics/claude-code-action@51ea8ea73a139f2a74ff649e3092c25a904aed7e # v1
235239
with:
236240
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
237241
claude_args: "--allowedTools Bash,Read,Glob,Grep,Edit,Write"

.github/workflows/claude-code-review.yml

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,23 @@ on:
66

77
jobs:
88
claude-review:
9-
if: ${{ !github.event.pull_request.draft && github.event.pull_request.user.login != 'dependabot[bot]' }}
9+
# Only review same-repo PRs. A fork PR runs attacker-controlled code in
10+
# this job, and the review prompt reads files from the checked-out tree
11+
# (CLAUDE.md etc), so a hostile fork could prompt-inject the agent while
12+
# CLAUDE_CODE_OAUTH_TOKEN sits in the environment with Bash enabled.
13+
if: >-
14+
${{ !github.event.pull_request.draft &&
15+
github.event.pull_request.user.login != 'dependabot[bot]' &&
16+
github.event.pull_request.head.repo.fork == false }}
1017
runs-on: ubuntu-latest
1118
permissions:
1219
contents: read
1320
pull-requests: write
1421
issues: read
22+
# claude-code-action mints an OIDC token to authenticate to the
23+
# Anthropic GitHub App. Required by the action itself. Safe here
24+
# because the job is gated to same-repo PRs (head.repo.fork == false),
25+
# so the token is never reachable from an untrusted fork PR.
1526
id-token: write
1627

1728
steps:
@@ -21,7 +32,10 @@ jobs:
2132
fetch-depth: 0
2233

2334
- name: Run Claude Code Review
24-
uses: anthropics/claude-code-action@v1
35+
uses: anthropics/claude-code-action@51ea8ea73a139f2a74ff649e3092c25a904aed7e # v1
36+
env:
37+
PR_NUMBER: ${{ github.event.pull_request.number }}
38+
REPO: ${{ github.repository }}
2539
with:
2640
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
2741
use_sticky_comment: true
@@ -34,33 +48,33 @@ jobs:
3448
3549
```bash
3650
# Dismiss all previous claude[bot] reviews
37-
gh api "repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/reviews" \
51+
gh api "repos/${REPO}/pulls/${PR_NUMBER}/reviews" \
3852
--paginate --jq '.[] | select(.user.login == "claude[bot]" and (.state == "CHANGES_REQUESTED" or .state == "COMMENTED")) | .id' \
3953
| while read -r review_id; do
40-
gh api -X PUT "repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/reviews/${review_id}/dismissals" \
54+
gh api -X PUT "repos/${REPO}/pulls/${PR_NUMBER}/reviews/${review_id}/dismissals" \
4155
-f message="Superseded by new review" -f event="DISMISS" 2>/dev/null || true
4256
done
4357
4458
# Delete all previous claude[bot] inline review comments
45-
gh api "repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/comments" \
59+
gh api "repos/${REPO}/pulls/${PR_NUMBER}/comments" \
4660
--paginate --jq '.[] | select(.user.login == "claude[bot]") | .id' \
4761
| while read -r comment_id; do
48-
gh api -X DELETE "repos/${{ github.repository }}/pulls/comments/${comment_id}" 2>/dev/null || true
62+
gh api -X DELETE "repos/${REPO}/pulls/comments/${comment_id}" 2>/dev/null || true
4963
done
5064
5165
# Delete all previous claude[bot] issue comments (except sticky)
52-
gh api "repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments" \
66+
gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" \
5367
--paginate --jq '.[] | select(.user.login == "claude[bot]" and (.body | test("<!-- claude-code-action") | not)) | .id' \
5468
| while read -r comment_id; do
55-
gh api -X DELETE "repos/${{ github.repository }}/issues/comments/${comment_id}" 2>/dev/null || true
69+
gh api -X DELETE "repos/${REPO}/issues/comments/${comment_id}" 2>/dev/null || true
5670
done
5771
```
5872
5973
After cleanup, proceed with the review below.
6074
6175
---
6276
63-
Review PR #${{ github.event.pull_request.number }} in ${{ github.repository }}.
77+
Review PR #${PR_NUMBER} in ${REPO}.
6478
6579
Read `CLAUDE.md` for project conventions. Pay special attention to:
6680
- The **CLI Design Principles** section (verb vocabulary, argument rules,

.github/workflows/claude.yml

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,30 @@ on:
1212

1313
jobs:
1414
claude:
15+
# The actor must be a trusted member of the repo (OWNER / MEMBER /
16+
# COLLABORATOR). Without this gate, any GitHub user can trigger an agent
17+
# with Bash + a write-scoped token by commenting "@claude" on this public
18+
# repo.
1519
if: |
16-
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
17-
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
18-
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
19-
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
20+
(github.event_name == 'issue_comment' &&
21+
contains(github.event.comment.body, '@claude') &&
22+
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)) ||
23+
(github.event_name == 'pull_request_review_comment' &&
24+
contains(github.event.comment.body, '@claude') &&
25+
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)) ||
26+
(github.event_name == 'pull_request_review' &&
27+
contains(github.event.review.body, '@claude') &&
28+
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.review.author_association)) ||
29+
(github.event_name == 'issues' &&
30+
(contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')) &&
31+
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.issue.author_association))
2032
runs-on: ubuntu-latest
2133
permissions:
2234
contents: write
2335
pull-requests: write
2436
issues: write
37+
# claude-code-action mints an OIDC token to authenticate to the
38+
# Anthropic GitHub App. Required by the action itself, not optional.
2539
id-token: write
2640
actions: read
2741

@@ -32,7 +46,7 @@ jobs:
3246
fetch-depth: 1
3347

3448
- name: Run Claude Code
35-
uses: anthropics/claude-code-action@v1
49+
uses: anthropics/claude-code-action@51ea8ea73a139f2a74ff649e3092c25a904aed7e # v1
3650
with:
3751
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
3852
claude_args: "--allowedTools Bash,Read,Glob,Grep,Edit,Write"

0 commit comments

Comments
 (0)