Skip to content
Cloud Security DeskSearch
Menu

Technical guideWorkload security

Stop pull_request_target workflows from running fork code

GitHub blocks pull_request_target in public repositories by default from November 2, 2026. Find the workflows that run fork code with secrets, split them, pin a checkout that refuses unsafe refs, and allow the rest deliberately.

Published
Sources checked
Next review
Reading time
16 minutes
Coverage
GitHub
A thick blue main line crosses the lower canvas while an amber branch leaves it, rises in a wide loop and curls back down toward a small white room on the main line that holds a mint key; a closed gate, a dark spruce bar with a red latch between two posts, stands across the end of the returning branch just before the room's open side.
Conceptual illustration: a fork branch curls back toward the room that holds the repository's credentials, and a closed gate stops it before the door.

An implementation guide for maintainers and platform teams whose GitHub Actions workflows react to fork pull requests, based on GitHub's changelog and Docs, the actions/checkout source and the Nx and Trivy incident records, reviewed October 9, 2026. It gives a dated record of the 2025 and 2026 defaults with what each leaves open, an inventory script, a split-workflow example and a scoped event policy.

At a glance

Key findings

  • From November 2, 2026, GitHub enforces a default rule that blocks pull_request_target in public repositories without an applicable Actions event policy; private and internal repositories, workflow_run and issue_comment are not covered. [1][3]
  • actions/checkout v7 refuses fork head and merge checkouts in pull_request_target and pull-request-driven workflow_run runs; floating major tags received the backport on July 20, 2026, but SHA, minor and patch pins did not. [4][5]
  • The checkout refusal does not inspect git fetch, gh pr checkout, artifacts, issue_comment or expressions in run lines, and an expression in a run line is where the Nx compromise began. [4][8][11]
  • pull_request_target workflows run regardless of fork approval settings and even with a merge conflict, so the approval button never stopped them. [7][6]
  • Trivy's maintainers trace their 2026 compromise to a vulnerable pull_request_target workflow exploited on February 27, and after a full credential reset their release-pipeline hardening starts with removing every such use. [16]

Why pull_request_target is different from pull_request

The workflows to find are the ones that pair a privileged trigger with pull request content. A run started by pull_request_target, by workflow_run after a fork's pull request, or by issue_comment gets the base repository's secrets and its GITHUB_TOKEN, and on a public repository anyone with an account can start one. It becomes a compromise when a step checks out the fork's head or merge commit and then builds, tests or installs it, fetches the same code with git or the gh CLI, runs something from an artifact, or writes a pull request title or comment into a shell script. GitHub's documentation calls the checkout, fetch and artifact forms a pwn request and lists command injection as a further risk in the same privileged context. [1][2]

On November 2, 2026, GitHub enforces a default event policy that blocks pull_request_target in public repositories that have no applicable Actions event policy. The rule has run in evaluate mode since workflow execution protections became generally available on September 17, 2026, so policy insights already list the runs it would stop. It skips private and internal repositories and does not touch workflow_run or issue_comment. A second change is already live: actions/checkout v7, generally available since June 18, 2026, refuses the common fork checkout patterns, and floating major tags such as @v4 got the same refusal on July 20, while workflows pinned to a commit SHA did not. [3][1][4][5]

Each workflow therefore needs one of three outcomes before November 2: move it to pull_request if it needs neither secrets nor a write token; split it into an unprivileged build and a privileged consumer if it needs both fork code and credentials; or keep pull_request_target for a job that never executes pull request content, and allow the event explicitly for that one workflow file.

The two events differ in what they trust. A pull_request run uses the workflow file from the pull request's merge commit, so a fork controls both the workflow and the code, and GitHub compensates with a read-only token, no other secrets and approval rules for outside contributors. A pull_request_target run takes its workflow file from the base repository's default branch, and so does any checkout that names no ref. Because only trusted code runs by default, the run gets the base repository's token, with whatever permissions the workflow or repository default grants, plus repository and organization secrets. [1][6]

Two documented behaviors make the trigger easier to misuse than it looks. Fork approval settings do not apply: GitHub considers the base branch trusted, so pull_request_target workflows always run, whatever the approval policy says, even for a first-time contributor. And they run when the pull request has a merge conflict, a state in which pull_request does not run at all. [7][6]

The checkout step that turns metadata into execution

The vulnerable shape is short. A pull_request_target job runs actions/checkout with ref: ${{ github.event.pull_request.head.sha }}, with refs/pull/<number>/merge, or with repository: set to the fork, and a later step runs make test or npm install in the workspace. The checkout executes nothing; the next step does, because the Makefile, package scripts, tests, build configuration and dependency install hooks now belong to whoever opened the pull request. GitHub's documentation names npm install and npm run build as commands that run attacker code without any obvious build step. [1][2]

The persisted token is the other half of the problem. persist-credentials defaults to true, so checkout leaves the job's token where git can use it. Since v6.0.0, released on November 20, 2025, that credential sits in a separate file under $RUNNER_TEMP rather than in .git/config, which keeps it out of a workspace a later step might upload, but it is still on the runner for any program a later step starts. Security Lab warned in 2021 that such a job hands fork code a token even when no secret is referenced. Set persist-credentials: false on every checkout in a privileged job. [5][2]

Checkout v7 adds a check before it fetches anything. When the event is pull_request_target, or workflow_run with an upstream pull request event, it compares the base repository's ID with the head repository's ID in the event payload. If they differ, it refuses when repository: names the fork, when ref: matches refs/pull/<number>/head or refs/pull/<number>/merge, or when the resolved commit equals the head or merge commit SHA in the payload. The step then fails with an error that begins Refusing to check out fork pull request code. Pull requests from branches in the same repository pass, the pull_request event is unchanged, and allow-unsafe-pr-checkout: true disables the check for one step. [4][8]

The first release produced false positives: on the closed event of a rebase-merged pull request the target branch head can equal the pull request head, and v7.0.0 refused even a checkout left at its defaults. v7.0.1, released July 20, 2026, skips the check for default inputs, so treat it as the floor for a pin. [9][5]

The check inspects the inputs and resolved commit of one action, and nothing outside that step:

  • A run step that fetches the pull request with git fetch or gh pr checkout and then builds it. GitHub's changelog names this gap. [4]
  • Events other than pull_request_target and workflow_run. The helper returns early for everything else, so an issue_comment command that starts tests on a pull request is never checked. [8]
  • Artifacts. A workflow_run consumer that downloads a build from the fork's pull_request run and executes it needs no checkout at all. [1]
  • Expression injection. A run line that contains ${{ github.event.pull_request.title }} executes shell written in the title, which is how the Nx compromise began in 2025. [10][11]
  • Repositories other than the fork. Pointing repository: at an unrelated third-party repository is not refused. [4]

A dated record of the 2025 and 2026 defaults

GitHub has been turning the same advice into defaults one route at a time. The timeline lists each change with its date and the gap it leaves, as recorded in the GitHub changelog, the GitHub Docs and the actions/checkout releases on October 9, 2026; the gaps are why none of them replaces a review of the workflow itself. [2][14][12][5][4][13][15][3]

The December 8, 2025 change closed the branch route. Before it, the base branch of a pull request supplied the workflow file and GITHUB_SHA, so a pull request aimed at an old release branch ran that branch's copy of the workflow, and GitHub's announcement says outdated workflows were exploited this way after being fixed on the default branch. Environment branch protection rules moved too: for pull_request_target they now evaluate against the default branch, which can break environments whose filters expected the head branch. [12]

Nx is the documented case of the old behavior. Its advisory says a validation workflow added on August 21, 2025 ran on pull_request_target and echoed ${{ github.event.pull_request.title }} in a run step. The team reverted it on master the next day and later reproduced how it was still reached: a pull request against an outdated branch that still carried the workflow. The team believes the workflow's token then started publish.yml from a malicious commit that sent the npm token to a webhook. Nx afterwards rebased every outdated branch, required approval for outside contributors and moved publishing to npm trusted publishing. A reasonable reading of GitHub's announcement is that the outdated-branch route stopped working on December 8, 2025; the injection would still work in a default-branch workflow, and checkout v7 would not have caught it, because no fork code was checked out. [11][12]

The 2026 record shows why GitHub went further than checkout. In their March 30, 2026 conclusion, Trivy's maintainers wrote that their compromise began on February 27, when an attacker exploited a workflow made vulnerable through a pull_request_target invocation in aquasecurity/trivy and exfiltrated organization and repository secrets. The attacker regained persistence despite credential revocations on March 1, and on March 19 used stolen credentials to build and distribute a malicious Trivy v0.69.4 through the project's release workflow. After a full credential reset, the maintainers' release-pipeline hardening starts with removing every vulnerable pull_request_target use and adding pipeline linters. [16]

Figure 01

Ten GitHub changes, each with a gap

Each default closes one route to fork code and leaves another open. [12][4][5][3]

Timeline of ten GitHub changes from August 3, 2021 to November 2, 2026. Each card names the change and what it leaves open, from Security Lab guidance and CodeQL general availability through the default-branch workflow source, checkout v6 and v7, read-only cache tokens, the July 20 backports and cache-mode to execution protections and the November 2 enforcement.

Source. GitHub Security Lab, GitHub changelog entries, GitHub Docs and the actions/checkout release history, reviewed October 9, 2026. [2][14][12][5][4][13][15][3][1]

Method. Dates are the announcement or release dates the sources publish; the default-branch change shows its announcement date and effective date. Leaves open restates limits the sources name, or follows from the scope each source states.

Accessible table and figure data
Figure 1 accessible table
DateChangeLeaves open
August 3, 2021Security Lab names the pwn request and the split patternadvice only; nothing is enforced
April 22, 2025CodeQL analysis of workflows becomes generally availablealerts still need a fix; about 15% were fixed in preview
November 7, 2025Default-branch workflow source announced, effective December 8a vulnerable workflow on the default branch itself
November 20, 2025checkout v6 moves the persisted token out of .git/configthe token stays on the runner for later steps
June 18, 2026checkout v7 refuses fork head and merge checkoutsgit or gh fetches, artifacts, issue_comment
June 26, 2026Untrusted triggers get read-only default-branch cache tokenswrite modes a workflow declares explicitly
July 20, 2026Refusal backported to the v2 to v6 major tagsSHA, minor and patch pins
September 10, 2026cache-mode generally available on all plansonly limits the cache, not code execution
September 17, 2026Execution protections GA; default rule in evaluate modeprivate and internal repositories
November 2, 2026Default rule enforced in affected public repositoriesworkflow_run, issue_comment and allowed workflows
Figure 1 accessible table
DateChangeLeaves open
August 3, 2021Security Lab names the pwn request and the split patternadvice only; nothing is enforced
April 22, 2025CodeQL analysis of workflows becomes generally availablealerts still need a fix; about 15% were fixed in preview
November 7, 2025Default-branch workflow source announced, effective December 8a vulnerable workflow on the default branch itself
November 20, 2025checkout v6 moves the persisted token out of .git/configthe token stays on the runner for later steps
June 18, 2026checkout v7 refuses fork head and merge checkoutsgit or gh fetches, artifacts, issue_comment
June 26, 2026Untrusted triggers get read-only default-branch cache tokenswrite modes a workflow declares explicitly
July 20, 2026Refusal backported to the v2 to v6 major tagsSHA, minor and patch pins
September 10, 2026cache-mode generally available on all plansonly limits the cache, not code execution
September 17, 2026Execution protections GA; default rule in evaluate modeprivate and internal repositories
November 2, 2026Default rule enforced in affected public repositoriesworkflow_run, issue_comment and allowed workflows

What changes on November 2, 2026

Workflow execution protections are Actions policies set at repository, organization or enterprise level: event rules allowlist the events that may start workflows, actor rules limit who may start them, and GitHub evaluates both before a run begins. General availability on September 17, 2026 added workflow file targeting, policy insights and a REST API. [3][17]

The default rule is narrow in where it applies and broad in what it blocks. It never replaces an event policy you already configured, but where it applies it blocks the event itself, not only fork pull requests, so a labeler for same-repository branches and a Dependabot auto-merge workflow on pull_request_target stop too. Runs continue while it is in evaluate mode; on November 2 GitHub enforces it for affected repositories that were using the default policy before general availability. [1][3][17]

Notice has been short each time. The chart counts the days between each announcement and the date its default took effect: about a month for the two earlier pull_request_target changes, 46 days for this one, and none for the read-only cache tokens, which applied on the day they were announced. [12][4][13][3]

Three points are not settled by the documentation reviewed. It does not say how a repository made public after September 17 is treated, how a policy scoped to one workflow file interacts with the default rule for the repository's other workflows, or whether the rule reaches GitHub Enterprise Server. Custom policies can use evaluate mode only on GitHub Enterprise Cloud, so elsewhere a policy you create is active or disabled from the start; check its effect in policy insights. [17][18]

Effect of the default pull_request_target rule, from the GitHub changelog and Docs reviewed October 9, 2026. [1][3]
Workflow or repositoryOn November 2Before then
Public, no event policy, uses pull_request_targetRuns blockedMigrate, split, or allow per workflow file
Public, with an applicable event policyDefault not applied; your policy decidesConfirm the policy says what you intend
Private or internalNo changeAdd your own event policy if wanted
Same-repository or Dependabot pull requestsBlocked with the eventMove Dependabot automation to pull_request
workflow_run and issue_comment workflowsNot blockedReview them with CodeQL or zizmor
Figure 02

Notice before each default took effect

Earlier pull_request_target changes gave about a month of notice, the November 2 rule 46 days, and read-only cache tokens none. [12][4][13][3]

Horizontal bar chart of calculated days from announcement to effect: pull_request_target default-branch source 31, checkout refusal on floating major tags 32, read-only cache for untrusted triggers 0, pull_request_target blocked in public repositories 46.

Source. Calculated from GitHub changelog entries of November 7, 2025, June 18, 2026, June 26, 2026 and September 17, 2026, reviewed October 9, 2026. [12][4][13][3]

Method. Days of notice = effective date minus announcement date, in calendar days: December 8 minus November 7, 2025 is 31; July 20 minus June 18, 2026 is 32 (the first date given, July 16, would be 28); June 26 minus June 26, 2026 is 0; November 2 minus September 17, 2026 is 46.

Accessible table and figure data
Figure 2 accessible table
Default changeDays of noticeAnnouncedIn effect
pull_request_target runs the default-branch workflow31November 7, 2025December 8, 2025
checkout refusal reaches floating major tags32June 18, 2026July 20, 2026
Read-only cache for untrusted triggers0June 26, 2026June 26, 2026
pull_request_target blocked in public repositories46September 17, 2026November 2, 2026
Figure 2 accessible table
Default changeDays of noticeAnnouncedIn effect
pull_request_target runs the default-branch workflow31November 7, 2025December 8, 2025
checkout refusal reaches floating major tags32June 18, 2026July 20, 2026
Read-only cache for untrusted triggers0June 26, 2026June 26, 2026
pull_request_target blocked in public repositories46September 17, 2026November 2, 2026

Find the risky workflows you already have

Start with the default rule's policy insights, because they list actual pull_request_target runs rather than files that merely mention the event. Then widen the search to the triggers the rule ignores. CodeQL default setup turns on workflow analysis automatically when the default branch contains workflow files, and its query actions/untrusted-checkout/critical treats pull_request_target, workflow_run and issue_comment as privileged; the same query pack covers code injection, artifact poisoning, cache poisoning and gated checkouts. During the preview GitHub reported more than 800,000 potential vulnerabilities across over 158,000 repositories, with about 15 percent fixed by maintainers, so treat an alert as the start of the work. [17][14][19][23]

zizmor, an open source workflow linter, flags pull_request_target and workflow_run in its dangerous-triggers audit. Its documentation adds issue_comment as of v1.31.0, which had not been released on October 9, 2026, when the newest release was v1.30.1. [20]

Across many repositories, a text search makes a first list to feed those tools. The fragment below runs over local clones, prints each workflow with a privileged trigger and the lines most likely to bring pull request content in, and lists SHA-pinned checkout references for the backport review.

For each hit, record four things: the trigger, the step that brings in pull request content, the permissions and secrets in that job, and whether the job writes the cache, deploys or can request an OIDC token. If run history shows that a vulnerable workflow already ran on a fork's pull request, scope it as a possible compromise, starting from the job logs.

Example triage fragment for local clones. It is a text search with false positives and misses; it does not replace CodeQL or zizmor.
# Example triage fragment (a text search, not a parser). Run it from a directory
# of local clones; confirm every hit with CodeQL or zizmor before acting on it.
shopt -s nullglob
for wf in */.github/workflows/*.yml */.github/workflows/*.yaml; do
  grep -qE '^[[:space:]]*(pull_request_target|workflow_run|issue_comment)[[:space:]]*:|^[[:space:]]*-[[:space:]]*(pull_request_target|workflow_run|issue_comment)[[:space:]]*$|^on:.*(pull_request_target|workflow_run|issue_comment)' "$wf" || continue
  echo "== $wf"
  # Steps that fetch, check out or download pull request content, and overrides.
  grep -nE 'pull_request\.head\.(sha|ref|repo)|refs/pull/|head_repository|gh pr checkout|git (fetch|checkout|pull)|download-artifact|allow-unsafe-pr-checkout|cache-mode: *write|persist-credentials' "$wf"
  # Attacker-written text expanded directly into the workflow.
  grep -nE '\$\{\{ *github\.(head_ref|event\.(pull_request\.(title|body)|comment\.body|issue\.(title|body)))' "$wf"
done

# SHA-pinned checkout references and their version comments, for the backport review.
grep -rhoE 'actions/checkout@[0-9a-f]{40}([[:space:]]*#[[:space:]]*v[0-9.]+)?' */.github/workflows | sort | uniq -c

# Commits of releases that carry the fork checkout refusal, resolved from upstream tags.
git ls-remote --tags https://github.com/actions/checkout v7.0.1 v6.1.0 v5.1.0 v4.4.0

Split privileged work from untrusted build steps

When a workflow must build fork code and also needs credentials, as with a coverage comment, a status check or tests against a private registry, the documented structure is two workflows. The first runs on pull_request, builds and tests the fork with a read-only token and no secrets, and uploads what the privileged side needs as an artifact. The second runs on workflow_run when the first completes, holds the write scope, downloads the artifact and treats its contents as data. Security Lab described the split in 2021 and refreshed its example on June 17, 2026; GitHub's events reference uses the same shape. [2][6]

Two rules keep the consumer safe. It never executes anything from the artifact, and it extracts into ${{ runner.temp }}, never into the workspace, where a file could replace a script a later step runs. It also validates every value, because a pull_request run uses the fork's own copy of the workflow file, so the fork decides what the artifact holds, the pull request number included. Security Lab accepts that risk for a job that only comments and offers an alternative: look the pull request up through the API by github.event.workflow_run.head_sha and the base branch. [2][6]

The fragment below is a hypothetical coverage-comment pair. The consumer has no checkout, one write scope and no cache access, and hands the comment body to gh as a file, so text from the fork never reaches a shell parser. If the privileged side deploys instead, keep id-token: write and environment secrets in a separate job that runs only for the default branch, and tie the cloud role's trust to that job.

The decision tree turns this into questions to answer for each job. Dependabot automation is the case where pull_request_target is most often used without need: GitHub's own examples for approving and merging Dependabot pull requests run on pull_request, with an explicit permissions block and a check that the author is dependabot[bot]. [21]

Example of the split pattern as two workflow files (hypothetical names). Every SHA is a placeholder; resolve each from the upstream tag in its comment. The artifact is attacker-controlled, so the consumer only reads it.
# .github/workflows/pr-coverage.yml: runs fork code, holds nothing worth stealing.
name: PR coverage
on:
  pull_request:
permissions:
  contents: read
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@1111111111111111111111111111111111111111 # v7.0.1
        with:
          persist-credentials: false
      - run: make coverage            # fork code runs here, with no secrets
      - name: Save data for the privileged workflow
        env:
          PR_NUMBER: ${{ github.event.number }}
        run: |
          mkdir -p out
          printf '%s\n' "$PR_NUMBER" > out/pr_number
          cp coverage/summary.txt out/summary.txt
      - uses: actions/upload-artifact@2222222222222222222222222222222222222222 # v7.0.2
        with:
          name: coverage
          path: out/
---
# .github/workflows/pr-coverage-comment.yml: holds one write scope, reads data only.
name: PR coverage comment
on:
  workflow_run:
    workflows: [PR coverage]
    types: [completed]
permissions: {}
jobs:
  comment:
    if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success'
    runs-on: ubuntu-latest
    cache-mode: none
    permissions:
      actions: read
      pull-requests: write
    steps:
      # No checkout: this job never needs the pull request's files.
      - uses: actions/download-artifact@3333333333333333333333333333333333333333 # v8.0.2
        with:
          name: coverage
          path: ${{ runner.temp }}/coverage   # never the workspace
          run-id: ${{ github.event.workflow_run.id }}
          github-token: ${{ github.token }}
      - name: Comment with the summary as plain data
        env:
          GH_TOKEN: ${{ github.token }}
          DATA: ${{ runner.temp }}/coverage
        run: |
          pr=$(head -c 12 "$DATA/pr_number")
          case "$pr" in ''|*[!0-9]*) echo "pr_number is not a number"; exit 1;; esac
          head -c 4000 "$DATA/summary.txt" > "$RUNNER_TEMP/body.txt"
          gh pr comment "$pr" --repo "$GITHUB_REPOSITORY" --body-file "$RUNNER_TEMP/body.txt"
Figure 03

Choose the trigger before hardening the job

Only a job that needs privileges and never runs pull request code belongs on pull_request_target. [1][2]

Decision tree with five questions: whether the job needs a secret or write token, whether it must build or run pull request code, whether the privileged job reads pull request text or files, whether the run is gated by a label, comment or approval, and whether the job writes the cache or deploys.

Source. Conceptual decision aid based on GitHub's pull_request_target guidance, Security Lab's split pattern, CodeQL's TOCTOU query help and GitHub's Dependabot automation examples. [1][2][22][21]

Method. Conceptual ordering of documented guidance. Answer the questions for each job, not for each workflow file, because one workflow can mix privileged and unprivileged jobs.

Accessible table and figure data
Figure 3 accessible table
QuestionYes, thenNo, then
Does the job need a secret or a write token?ask whether it must run pull request codeuse pull_request; fork runs get a read-only token
Must it build or run pull request code?split it: build on pull_request, act on workflow_runpull_request_target can fit; allow it for that file
Does the privileged job read pull request text or files?pass text through env and treat files as dataskip checkout and artifacts entirely
Is the run gated by a label, comment or approval?check out the approved SHA, never a branch refrely on the event policy and token scope
Does the job write the cache or deploy?move that to a push or environment-gated jobset cache-mode to none and grant one scope
Figure 3 accessible table
QuestionYes, thenNo, then
Does the job need a secret or a write token?ask whether it must run pull request codeuse pull_request; fork runs get a read-only token
Must it build or run pull request code?split it: build on pull_request, act on workflow_runpull_request_target can fit; allow it for that file
Does the privileged job read pull request text or files?pass text through env and treat files as dataskip checkout and artifacts entirely
Is the run gated by a label, comment or approval?check out the approved SHA, never a branch refrely on the event policy and token scope
Does the job write the cache or deploy?move that to a push or environment-gated jobset cache-mode to none and grant one scope

When pull_request_target has to stay

Some jobs need the base repository's token on a fork's pull request and never touch its code: labeling, triage, welcome comments, a status built from event metadata. To keep one running after November 2, create an event policy that allows pull_request_target and scope it with workflow file targeting to the files that need it, so the allowance does not cover a workflow someone adds later. The JSON below is an example body for POST /repos/{owner}/{repo}/actions/policies, which needs a token with repository Administration write permission; the Policies page in the repository settings builds the same policy. [18][3]

The API describes allowed_events as the events that can trigger workflows, so it follows that a policy scoped to a workflow file blocks any event that file uses and the policy leaves out. If the labeler also runs on push or workflow_dispatch, list those too, then confirm in insights that only the intended runs pass. [18]

Then shrink the job. Grant the single scope it uses, such as pull-requests: write. Skip checkout when the event payload is enough; otherwise check out the default branch with persist-credentials: false. Pass pull request text to scripts through env and quote it in the shell, never inside ${{ }} in a run line. Leave cache-mode at its read-only default or set it to none, and run the job on GitHub-hosted or other ephemeral runners, since GitHub's guidance for this trigger asks for isolated compute that is not reused between runs. [1][10][15]

GitHub's documentation also permits checking out fork code that is only ever inspected as data and never executed, with allow-unsafe-pr-checkout: true named so that it stands out in review. zizmor's maintainers disagree that data-only handling is safe in general, listing argument injection, environment injection through variables such as LD_PRELOAD, and file inclusion through links to the runner's credentials as ways fork content still gets executed. For a job that holds a secret, follow the stricter view; if privileged inspection is unavoidable, give the job one narrow scope and no secrets, check out into a subdirectory with path:, and name the head SHA from the event payload instead of a branch. [1][20]

Label and approval gates need the same immutable reference. Security Lab calls a safe to test label gate a temporary fix, because the author can push new commits after the label goes on and before the run starts, and CodeQL's actions/untrusted-checkout-toctou/critical query flags gated workflows that check out a branch ref instead of the approved commit. [2][22]

Example repository Actions policy that allows pull_request_target for one workflow file (placeholder path). An event rule is an allowlist: list every event that file uses, and check policy insights after creating it.
{
  "name": "Allow pull_request_target for the labeler only",
  "enforcement": "active",
  "conditions": {
    "workflow_path": {
      "include": [
        ".github/workflows/label-pull-requests.yml"
      ],
      "exclude": []
    }
  },
  "rules": [
    {
      "type": "restrict_action_events",
      "parameters": {
        "allowed_events": [
          "pull_request_target"
        ]
      }
    }
  ]
}

SHA pinning and the checkout v7 backport

On July 20, 2026, GitHub published actions/checkout v6.1.0, v5.1.0, v4.4.0, v3.7.0 and v2.8.0, each labeled as a breaking backport of allow-unsafe-pr-checkout, together with v7.0.1. A git ls-remote on October 9 and again on October 10 showed each of the v2 to v6 major tags on the same commit as its backport release. So actions/checkout@v4 refuses fork checkouts today, while a commit SHA, a minor tag or a patch tag does not, and GitHub's changelog says such workflows need an upgrade through Dependabot or an established upgrade process. Version 1 received no backport. [5][4]

This is the direct tension with SHA pinning, which remains the right default for actions you do not control. The pin did what it promises: the code under it did not change, and here the change it held back was a security fix. Treat the backport as an update a pinned estate pulls in on purpose. Resolve the SHA of v7.0.1 or later from the upstream tag, or of the backport release for a workflow that cannot change major version yet, and roll it out through Dependabot or a reviewed bulk change; the last line of the inventory fragment prints those commits.

Expect the bump to break something, and read the break as a finding. A workflow that fails at checkout with the refusal was checking out fork code with privileges, and the fix is the split pattern or a narrower job, not an allow-unsafe-pr-checkout line added to get the build green. Watch review for that input and for write-capable cache-mode values on low-trust triggers, which also override a safe default and draw a warning annotation from GitHub. [4][15]

Verify with CodeQL and a controlled fork

Static analysis finds patterns; a test shows where the boundary sits. Use a disposable fork owned by a second account with no write access, give the base repository throwaway secrets only, and record the date and checkout version with each result. Each case should end in a specific, visible failure:

  • A pull request whose build script writes a marker file. No privileged run may show the marker.
  • A test branch that points checkout at ${{ github.event.pull_request.head.sha }} under pull_request_target. The step should fail with the refusal message. [8]
  • The same fetch done with git fetch in a run step. Checkout will not catch it, so code review and CodeQL have to. [4][19]
  • A pull request title and a comment that carry a shell payload. No command output may appear in any log. [10]
  • An artifact from the fork's pull_request run that includes an executable and an unexpected extra file. The consumer must ignore both. [2]
  • After November 2, a pull_request_target run with no allowance, which should fail, and the allowed workflow, which should still run. [1]

What to finish before November 2

The order assumes public repositories. In private and internal repositories nothing happens on November 2, but checkout v7 still refuses fork checkouts there, because its check looks only at the event and the repository IDs. [8] Every step except the event policy therefore still applies wherever fork pull requests are accepted. Work in this order, because each step narrows the next one:

  • Read the default rule's policy insights for every public repository and list each pull_request_target workflow that ran since September 17, 2026.
  • Decide each one: pull_request, the split pattern, or kept and allowed for that workflow file only. Delete workflows nobody owns.
  • Bump every SHA-pinned actions/checkout to v7.0.1 or later, or to the backport release for its major version.
  • Review workflow_run and issue_comment workflows, and every run line, for fork fetches and expanded pull request text, which the November rule leaves alone.
  • Create the scoped event policies before November 2, then watch for blocked runs and for new allow-unsafe-pr-checkout or write cache-mode lines in review.

Method and provenance

Source-led implementation analysis of GitHub changelog entries, GitHub Docs, the GitHub REST API reference, the actions/checkout source and release history, CodeQL and zizmor documentation, and the Nx and Trivy maintainers' incident records. Sources were reviewed on October 9, 2026 and rechecked on October 10, 2026.

No repository, fork, policy or workflow was configured or run for this analysis. Behavior is bounded to the cited documentation and source code as of the review date, and the November 2, 2026 enforcement had not yet happened when the sources were reviewed.

AI assistance. AI assisted research synthesis, drafting, diagram planning and visual production, with deterministic editorial checks. No personal deployment experience, independent human review or live test is claimed.

Published under the Cloud Security Desk organizational byline. Read the practitioner guide policy.

References

  1. Securely using pull_request_target GitHub. Accessed .
  2. Keeping your GitHub Actions and workflows secure Part 1: Preventing pwn requests GitHub Security Lab. Published . Accessed .
  3. Workflow execution protections in GitHub Actions generally available GitHub. Published . Accessed .
  4. Safer pull_request_target defaults for GitHub Actions checkout GitHub. Published . Accessed .
  5. actions/checkout releases GitHub. Accessed .
  6. Events that trigger workflows GitHub. Accessed .
  7. Managing GitHub Actions settings for a repository GitHub. Accessed .
  8. unsafe-pr-checkout-helper.ts in actions/checkout GitHub. Accessed .
  9. Unsafe PR checkout assertion breaks with rebase merges (actions/checkout issue 2511) actions/checkout issue tracker. Published . Accessed .
  10. Script injections GitHub. Accessed .
  11. Malicious versions of Nx and some supporting plugins were published (GHSA-cxm3-wv7p-598c) Nx (nrwl). Published . Accessed .
  12. Actions pull_request_target and environment branch protections changes GitHub. Published . Accessed .
  13. Read-only Actions cache for untrusted triggers GitHub. Published . Accessed .
  14. GitHub Actions workflow security analysis with CodeQL is now generally available GitHub. Published . Accessed .
  15. Control GitHub Actions cache access with cache-mode GitHub. Published . Accessed .
  16. Trivy Security incident 2026-03-19 conclusion (aquasecurity/trivy discussion 10462) Aqua Security Trivy maintainers. Published . Accessed .
  17. REST API endpoints for GitHub Actions policies GitHub. Accessed .
  18. zizmor audit rules zizmor project. Accessed .
  19. Automating Dependabot with GitHub Actions GitHub. Accessed .
  20. Untrusted Checkout TOCTOU (CodeQL query help) GitHub. Accessed .
  21. CodeQL query help for GitHub Actions GitHub. Accessed .