Build1 publisher3 min readPublished
Scanner authors find six exploitable GitHub workflows among 60 that use pull_request_target
Scanner authors writing on dev.to hand-checked 60 public pull_request_target workflows and judged 6 exploitable by anyone who can open a pull request. About as many looked dangerous and were safe, so an audit has to read what runs after the checkout.
The Engineer · Build desk
Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction

What happened
- In the first run of 30 workflows, seven of the rule's eight false alarms used environment:, the gate GitHub documents for holding a job until a human approves it.
- A second sample of 30 turned up six workflows that check out pull request code; five were safe through unprivileged paths or a maintainer label gate.
- The sixth ran dotnet build and the tests on contributor code with contents: write and pull-requests: write, so anyone opening a pull request could push to the repository.
- That workflow declared allow-unsafe-pr-checkout: true, and the authors' rule stayed silent because they had been treating the line as a mitigation.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- cost A rule that matches the trigger and the checkout without looking for an approval gate sends critical-bug reports to maintainers who already deployed GitHub's documented fix.
- exposure A workflow with all three ingredients and no gate exposes every secret it can reach, so owners who find one have to rotate those secrets as well as change the trigger.
- constraint Scanners that read allow-unsafe-pr-checkout: true as a safe signal will skip the workflows whose owners chose to run fork code with a write token.
When a fork opens a pull request against a workflow triggered by `pull_request_target`, the job runs in the base repository's context. It gets that repository's secrets and a token that can write [1]. On its own, that is fine [1]. A takeover needs two more ingredients [2]. One is a checkout of the pull request's own code, through `ref: ${{ github.event.pull_request.head.sha }}` or `.head.ref`. The other is a step that executes it: install scripts, a build, the test suite [2]. Take away any one of the three and the attack is gone, according to the post's authors [2]. They ship a scanning rule for that combination and tested it on public workflows twice, two weeks apart [3].
The first run flagged 15 workflows [4]. dbt-labs and puppetlabs were among the false alarms, and both had the `environment:` gate in place with a comment explaining it in their own files [5]. Another, cpprefjp/site, runs its scripts from a second checkout of the default branch kept in a separate folder, so the contributor's code is only read as data [6]. "Had we opened issues from that first run, we would have told seven well-known projects they had a critical bug they did not have," the authors wrote [7].
The approval gate accounts for the first run's false alarms; for the second sample, the post lists different reasons [5][10][11]. Three of the safe workflows there never ran contributor code with privileges [10]. In those, the privileged trigger fired only on a closed pull request, the checkout was the base branch, or the head branch appeared only in a concurrency group name [10]. The other two, ecmwf's cfgrib and thermofeel, hold fork pull requests until a maintainer adds an `approved-for-ci` label [11].
The miss in that sample matters more for anyone writing detection. The cpprefjp case is where the line got its trust [13]. According to the post, actions/checkout v7 requires `allow-unsafe-pr-checkout: true` precisely so the user acknowledges the risk [14]. "Declaring a risk does not remove it," the authors wrote [15]. The rule now honors the declaration only if the job executes no code, or if the code it executes was checked out from the trusted base branch [16]. I think that condition is correct: it asks where the executed code came from. The cpprefjp workflow passes it and the `dotnet build` workflow fails it [6][12][16].
Even the corrected rule needs a reader behind it. After the first fix it fired on 8 workflows in the first sample, and the authors confirmed 5 by hand [8]. That is a hit rate of 62.5 percent [1]. Those five plus the second sample's one are the six exploitable workflows [2]. The post's audit starts with two greps [19]:
``` grep -rln "pull_request_target" .github/workflows/ grep -rn "github.event.pull_request.head" .github/workflows/ ```
A file that shows up in both needs reading past the checkout [19]. If the job needs no secrets, switching to `pull_request` removes the problem [18]. If it does, the post's alternatives are an approval environment, a maintainer label, or a trusted base-branch checkout that runs only its own scripts [18]. The remaining option splits the job: a `pull_request` workflow builds and uploads artifacts, and a privileged `workflow_run` workflow consumes only those artifacts [18].
The authors put limits on the 10 percent figure themselves [17]. "Sixty workflows is a sample, not a census," they wrote [20]. GitHub code search does not return a uniform random slice of every repository, so the figure shows the mistake is common enough to check for, not how common it is across GitHub, according to the post [21]. Each case was judged by reading the workflow, not by running an exploit [22]. The exposed repositories are not named; the authors report through private channels where a project offers one [23].
What to watch
- Whether the unnamed repositories, including the dotnet build workflow with contents: write, change their triggers after private reports.
- A uniformly sampled count of pull_request_target workflows; only that would show whether 10 percent holds beyond GitHub code search results.
- Whether other workflow scanners treat allow-unsafe-pr-checkout: true as a mitigation, the mistake this rule made and then corrected.