You review the code in a pull request. Your CI runs the workflow that the repository defines for it, and that workflow is where the trust decisions actually happen: what token it holds, what it checks out, what it caches, what it persists. Four failure patterns cover most of the real incidents.
1. The pwn request
pull_request_target exists so workflows can comment on fork PRs: it runs with a read-write token in the context of the base repository. The moment such a workflow also checks out the PR head and executes anything from it, a fork controls code that runs with your repository's credentials:
on: pull_request_target # elevated token
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }} # attacker code
- run: ./build.sh # executed with it
This combination is GHA_PWN_REQUEST_001. Either half alone is fine. Together they are the canonical workflow takeover.
2. Cache poisoning across the fork boundary
Actions caches are keyed, shared, and writable from PR runs in more configurations than people expect. A fork PR that can write a cache entry which a trusted run later restores has a code path into your main-branch builds. The trust boundary you need to reason about is restore versus save, and who could have written what your trusted job restores (GHA_CACHE_001).
3. OIDC tokens are credentials too
Workflow OIDC tokens trade long-lived secrets for short-lived federation, which is good, and they also mean any code execution inside the job can mint cloud credentials while it runs. A job that exposes id-token: write to steps that run untrusted code has handed the federation surface to that code (GHA_OIDC_001).
4. Credentials that outlive the step
actions/checkout persists its token into the local git config by default. Later steps, including anything an attacker reaches, can read it back. On PR head refs that default deserves to be off (GHA_CHECKOUT_001).
Reviewing for this by hand does not scale
These four patterns are visible in workflow YAML, which makes them checkable before merge. Aguara's ci-trust analyzer parses .github/workflows/*.yml and flags the combinations, not the individual features: pull_request_target alone is not a finding; the chain is.
$ aguara scan .github/workflows
HIGH .github/workflows/ci.yml
GHA_PWN_REQUEST_001: pull_request_target + head checkout + execution
It runs offline and the rules are versioned with the binary, so the same workflow gets the same verdict in review, in CI, and six months from now.
Scan your workflows before the next fork PR does
aguara scan .github/workflows runs the trust-chain checks locally or in CI.