Skip to content

fix(ci): dicht ongeauthenticeerde @claude-trigger (RCE op publieke repo)#25

Merged
anneschuth merged 4 commits into
mainfrom
fix/ci-unauthenticated-claude-trigger
May 18, 2026
Merged

fix(ci): dicht ongeauthenticeerde @claude-trigger (RCE op publieke repo)#25
anneschuth merged 4 commits into
mainfrom
fix/ci-unauthenticated-claude-trigger

Conversation

@anneschuth

Copy link
Copy Markdown
Member

Kwetsbaarheid

.github/workflows/claude.yml startte de Claude-agent zodra een issue, issue-comment, PR-review of PR-review-comment de tekst @claude bevatte. De if:-conditie controleerde alleen of die string aanwezig was, niet wie de auteur is.

Deze repo is publiek. Gevolg: elke GitHub-gebruiker kon door één comment te plaatsen een agent starten met:

  • --allowedTools Bash,Read,Glob,Grep,Edit,Write (shell + schrijftoegang)
  • contents: write, pull-requests: write, issues: write, id-token: write
  • het geïnjecteerde secrets.CLAUDE_CODE_OAUTH_TOKEN

Een aanvaller kon daarmee code uitvoeren in de runner, het GITHUB_TOKEN en het Claude-OAuth-token exfiltreren, naar de repo pushen, en via id-token OIDC pivoten naar cloud-trust die deze repo-identiteit accepteert. Aanvalsoppervlak: het hele internet, één comment, geen voorafgaande toegang.

Fix

claude.yml draait de job nu alleen als de auteur OWNER, MEMBER of COLLABORATOR is. De gate staat op alle vier de triggers (issue_comment, pull_request_review_comment, pull_request_review, issues), telkens naast de bestaande @claude-check.

claude-code-review.yml is defensief meegehard (geen aparte kwetsbaarheid: die workflow draait op pull_request met contents: read en zonder Write-tool, dus een fork-PR krijgt een read-only token). PR-metadata loopt nu via env:-variabelen in plaats van directe ${{ }}-interpolatie in het Bash-blok, zodat script-injectie via PR-titel/-velden structureel niet meer kan.

Vervolgactie buiten deze PR

CLAUDE_CODE_OAUTH_TOKEN moet als mogelijk gecompromitteerd worden behandeld en geroteerd worden (gebeurt out-of-band, niet in deze PR). De fix sluit het gat, maar niet eventuele reeds gelekte token-waarde.

Test

  • Beide workflows parsen (lokaal gevalideerd met PyYAML).
  • Comment @claude als niet-collaborator → job draait niet.
  • Comment @claude als collaborator/member → job draait zoals voorheen.

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.
@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.
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.
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.
@anneschuth anneschuth merged commit 2abf017 into main May 18, 2026
7 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant