Skip to main content

What the check does

Two findings. MEDIUM — the workflow is bound to both pull_request and pull_request_target. Anchored on the pull_request_target entry, since that is the one to remove. HIGH — a pull_request_target or workflow_run job reads github.event.…head.…. Both spellings are matched: the pull_request event’s nested head object, and workflow_run’s flattened head_sha / head_ref / head_branch / head_commit / head_repository. One finding per job.

Why it matters

The trigger names differ by one word and the security models are opposites: Bound to both, every job runs twice under two different models, and a reader has to hold both in their head to reason about any one job. The usual history is someone adding pull_request_target to make secrets available and forgetting to remove the original — at which point the privileged copy is the one an attacker targets, and nobody is looking at it. The second finding is the bridge itself. pull_request_target runs with repository secrets and a write-scoped token; github.event.pull_request.head.* is content the pull-request author controls. Where the two meet is the classic pwn-request:
This is deliberately broader than CICD-SEC-1 dangerous checkout, which fires only on actions/checkout with an untrusted ref:. Head content reaches a job through an if: guard, another action’s with: input, or a job env: just as well — and none of those are checkouts.

Safe alternative

Keep one trigger. Use pull_request for anything that builds or tests contributor code — it gets no secrets, which is the point. If a job genuinely needs secrets on a fork PR, move just that job into a separate pull_request_target workflow that does not check out or execute head content, and gate it on a label or an approving review:
Read the head ref only to post a comment or set a status — never to check out, build, execute, or interpolate into a shell.

What does not fire

  • Either trigger alone. pull_request_target on its own is a deliberate choice, not a confusion; other rules cover what it then does.
  • A head-context read under a plain pull_request — there are no secrets and no write token for it to reach.

Auto-fix

Partial, and scoped to the trigger block. Dropping pull_request_target changes which secrets the workflow can see; dropping pull_request changes when it runs at all; splitting the file is a refactor, not a rewrite. Any of those could silently break a release path, so the fix annotates the trigger that needs the decision and leaves the decision to a human. Both triggers stay in place, and running the fix twice does not stack a second comment. The head-context finding gets no fix — there is nothing mechanical to do about a job reading untrusted input.