Skip to content

security(ci): repo-guard's issue_comment path checks out any fork's head based on the commenter, not the PR author #441

Description

@ceilf6

Context

Split out of #437 per the repo-guard review on #438. Pre-existing behavior, documented but deliberately not changed there — #438 only corrected the description.

Behavior

.github/workflows/repo-guard.yml gates issue_comment on the commenter:

github.event_name == 'issue_comment' &&
(
  contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association) ||
  contains(fromJSON('["NanluQingshi","HaveNiceDa"]'), github.event.comment.user.login)
)

The checkout step then resolves that branch to:

ref: ${{ ... || format('refs/pull/{0}/head', github.event.issue.number) }}

So any MEMBER/COLLABORATOR (or the two named external contributors) commenting on any third-party fork PR pulls that fork's head onto the self-hosted runner (vars.REPO_GUARD_RUNNER), in a job whose env carries secrets.LLM_API_KEY.

By contrast the pull_request_target path gates on the PR author and is restricted to repo branches plus the named allowlist.

Why this is not covered by the #438 opt-in

actions/checkout's assertSafePrCheckout returns early unless the event is pull_request_target or workflow_run, so the issue_comment path never hit the refusal and is unaffected by allow-unsafe-pr-checkout. #438 scopes that flag to pull_request_target only, so this path stays opted out even if the action's guard widens later.

Mitigating context

Confirmed from ceilf6/repo-guard@main's action.yml: the composite steps only ever execute from github.action_path (node "${{ github.action_path }}/scripts/review.mjs"), never from the checked-out workspace, which is passed as an env var and documented as explored "with read-only tools". So the exposure is not arbitrary code execution.

The residual risk is prompt injection against the reviewing agent — fork-authored files are read by Claude Code on a persistent host with an API key in env.

Also note the trigger requires a deliberate act by a trusted member, which is itself a form of vetting.

Options

  1. Leave as-is, now that it is documented in the workflow comment.
  2. Additionally require the PR author to pass the same allowlist on this path — needs an API lookup, since the issue_comment payload's issue.pull_request carries only URLs and no head repo info.
  3. Resolve the head SHA via gh api first and check out that fixed SHA, closing the TOCTOU gap that refs/pull/{n}/head leaves open.

Maintainer call — this is a trust decision, not a bug fix.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions