Skip to content

fix: refuse Claude reviews of fork pushes made after the comment [COMP-2904] - #741

Open
cristianrcv wants to merge 2 commits into
masterfrom
fix/COMP-2904-untrusted-checkout-privileged-context
Open

cristianrcv wants to merge 2 commits into
masterfrom
fix/COMP-2904-untrusted-checkout-privileged-context

Conversation

@cristianrcv

@cristianrcv cristianrcv commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fixes code-scanning alert #15 (CodeQL actions/untrusted-checkout/high, "Checkout of untrusted code in a privileged context", trigger issue_comment) in .github/workflows/claude.yml.

The gap. The job's author_association gate vouches for the commenter, not for the PR head. On a fork PR, the author can push a new commit between a maintainer's @claude comment and the job start. The run then checks out code nobody with write access has seen, in a job that holds ENG_ANTHROPIC_API_KEY and a pull-requests: write token. The existing "resolve the SHA once" step (#681) only closed the shorter window between resolving and checkout.

The fix. The resolve step now stops the run on a fork PR when the fork's pushed_at is at or after the triggering comment or review:

  • pushed_at is set by GitHub on push. Commit dates are set by the author and can be backdated.
  • It is read after the head SHA, so a push in between also stops the run (fail closed).
  • Same-repo PRs skip the check, because their pushers already have write access. The automatic pull_request review has no comment and is already limited to same-repo PRs.
  • A deleted fork, an unresolvable SHA or an API error all fail the step.
  • Inputs reach the script through env: rather than inline ${{ }} expressions.

This is the comment-vs-head date check that CodeQL's query requires for issue_comment checkouts, in addition to the actor/association check the job already has (UntrustedCheckoutHigh.ql, BashCommentVsHeadDateCheck in ControlChecks.qll).

Why this and not something bigger. The checkout stays, so the review keeps reading the PR's own files, which was the reason for adding it. Alternatives I considered:

  • Gating the job on a deployment environment: would also satisfy CodeQL, but every @claude would then wait for a manual approval.
  • Checking out the base branch instead would make Claude read stale copies of the changed files.
  • Dismissing the alert again: Manage workspaces #14 was dismissed as "won't fix", and the alert came back as Manage teams #15 once the lines moved. The underlying race was also real.

Already mitigated, unchanged here. At the pinned version, claude-code-action restores .claude/, .mcp.json, CLAUDE.md and similar files from the base branch before the CLI starts (src/github/operations/restore-config.ts), so a PR cannot plant hooks or MCP servers. Claude's tools are limited to Read/Grep/Glob/Write and scoped gh pr verbs.

Behaviour change. If a fork is pushed after a maintainer comments, the run fails with an annotation asking them to review the latest push and mention @claude again. Any push to the fork counts, including other branches and "Sync fork", which errs on the safe side.

Verification

  • actionlint (with shellcheck) passes.
  • I ran the step's script, extracted from the YAML, against the live API with a date -d <iso> +%s shim (no GNU date or Docker locally):
  • CodeQL on this PR reports 0 actions results, against 1 (alert Manage teams #15) on master. PR analyses here are diff-scoped, so the second commit adds a one-line comment inside the checkout step to bring the alert's own location into the diff (5 diff ranges, including line 104 in the checkout step). Whether that scoping matches on overlap or on start line only is my inference, not documented, so the merge run below is the real proof.
  • After merge, alert Manage teams #15 closes as fixed

The Claude review cannot review this PR itself, because the workflow file differs from the default branch. That is expected.

Other squad repos run the same template (DEVOPS-1775) and probably need the same change.

JIRA

COMP-2904: Fix Checkout of untrusted code in a privileged context

Security Advisory

https://github.com/seqeralabs/tower-cli/security/code-scanning/15

🤖 Generated with Claude Code

cristianrcv and others added 2 commits November 9, 2026 11:58
…(COMP-2904)

The author_association gate vouches for the commenter, not for the PR
head. On a fork PR the author could push a new commit between a
maintainer's @claude comment and the job start; the run would then check
out and hand Claude code nobody with write access had seen, in a job that
holds the Anthropic API key and a pull-requests: write token. Resolving
the SHA once only closed the shorter gap between resolving and checkout.

The resolve step now stops the run when the fork's pushed_at is at or
after the triggering comment or review. pushed_at is set by GitHub on
push, unlike commit dates, and is read after the SHA so a push in between
also stops the run. Same-repo PRs skip the check because their pushers
already have write access, and the automatic pull_request review has no
comment to compare against.

This is the comment-vs-head date check CodeQL requires for issue_comment
checkouts (actions/untrusted-checkout/high, code-scanning alert 15).
Values reach the script through env instead of inline expressions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…P-2904)

Makes the link between the date-checked resolve step and the checkout
explicit, so nobody swaps the ref back to the mutable PR branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cristianrcv cristianrcv changed the title fix(ci): refuse Claude reviews of fork pushes made after the comment (COMP-2904) fix: refuse Claude reviews of fork pushes made after the comment (COMP-2904) Nov 9, 2026
@cristianrcv cristianrcv changed the title fix: refuse Claude reviews of fork pushes made after the comment (COMP-2904) fix: refuse Claude reviews of fork pushes made after the comment [COMP-2904] Nov 9, 2026
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