What the check does
Two findings. MEDIUM — the workflow is bound to bothpull_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:
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. Usepull_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:
What does not fire
- Either trigger alone.
pull_request_targeton 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. Droppingpull_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.