On this page · 11 sections
- What changes on 2 December 2026
- Why GitHub is blocking it: the pwn request
- Step 1: find every affected workflow
- Step 2: triage each workflow
- Step 3: replace the trigger with safe patterns
- Step 4: allow what must stay with a scoped event policy
- An organisation-wide fix plan
- Worked example: an organisation with 60 public repositories
- India and Enterprise Server notes
- How eCorpIT can help
- References
Summary. From 2 December 2026, GitHub Actions blocks pull_request_target events by default in public repositories on GitHub Free, GitHub Team and GitHub Enterprise Cloud, unless an applicable Actions event policy explicitly allows them. Enforcement was first set for 2 November 2026; GitHub moved it to 2 December in an editor's note dated 21 October and confirmed the new date with a brownout schedule on 2 November. The first brownout ran on 9 November. The last one is on Monday 16 November 2026, 16:00 to 18:00 UTC (21:30 to 23:30 IST), when blocked runs fail with an error annotation and the workflow does not execute. Private and internal repositories and GitHub Enterprise Server are not affected. The reason is the pwn request: a pull_request_target job holds the base repository's secrets and a read/write GITHUB_TOKEN, so running a fork's code in it hands both to the pull request author. For most teams the fix is not to allow the trigger back. Move build and test jobs to pull_request, split jobs that need secrets into an unprivileged pull_request half and a privileged workflow_run half, and allow pull_request_target only for narrow jobs such as labelling, through a policy scoped to that workflow file.
About this article. For platform, DevOps and security teams and open-source maintainers with public GitHub repositories. We checked dates, scope, behaviour, action versions and plan limits against the GitHub Changelog, GitHub Docs, the GitHub Security Lab, official actions/* release pages and GitHub's pricing page on 11 November 2026. GitHub has moved this date once, so re-check before 2 December. No vendor paid for inclusion. Researched with AI assistance and reviewed by our editorial team.
What changes on 2 December 2026
The block is a default rule inside workflow execution protections, the policy layer in the Policies section of Actions settings. GitHub opened that feature as a public preview on 18 June 2026 and made it generally available on 17 September 2026, adding workflow file targeting, policy insights and a REST API. The default rule currently runs in evaluate mode: workflows still run, and GitHub records the runs it would block.
The timeline below comes from GitHub's 2 November changelog post and the 17 September GA post.
| Date (2026) | What happens | Time |
|---|---|---|
| 17 September | Default rule announced with workflow execution protections GA, enforcement set for 2 November | Not applicable |
| 21 October | Editor's note moves enforcement to 2 December | Not applicable |
| 2 November | Changelog confirms 2 December and publishes two brownouts | Not applicable |
| 9 November | Brownout 1, the default rule enforced temporarily (passed) | 16:00 to 18:00 UTC |
| 16 November | Brownout 2, the last temporary enforcement | 16:00 to 18:00 UTC, 21:30 to 23:30 IST, 17:00 to 19:00 CET, 11:00 to 13:00 EST |
| 2 December | Permanent enforcement in affected public repositories | Time not published |
Who is in scope, according to GitHub's changelog and its pull_request_target reference page:
| Repository or product | Affected on 2 December? |
|---|---|
| Public repositories on GitHub Free, Team or Enterprise Cloud | Yes, unless an applicable event policy allows pull_request_target |
| Private repositories | No |
| Internal repositories | No |
| GitHub Enterprise Server | No |
| Public repositories with an applicable event policy you already configured | The default rule does not replace that policy |
A blocked run fails with an error naming the disallowed event and the workflow file. Enforcement applies to affected repositories that were on the default pull_request_target policy before general availability. If you created event policies during the preview, check what they allow, because an applicable policy you configured takes the place of the default.
Why GitHub is blocking it: the pwn request
A pull_request workflow runs the workflow file from the pull request's merge commit. For a fork, someone without write access controls that commit, so GitHub gives the job a read-only GITHUB_TOKEN, withholds other secrets and applies fork approval policies.
pull_request_target changes the source: the workflow file, and any actions/checkout step without a ref, come from the base repository's default branch. Because only trusted code runs, GitHub grants secrets and a read/write token, which suits labelling, triage and commenting on fork pull requests. Since 8 December 2025 the workflow source is always the latest commit on the default branch, whatever the pull request's base branch.
The danger appears when a workflow author overrides that default to test the fork's code with secrets available:
# INSECURE. Do not copy. Shown to recognise the pattern.
on:
pull_request_target:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7 # v7 now refuses this ref unless allow-unsafe-pr-checkout is true
with:
ref: ${{ github.event.pull_request.head.sha }}
- run: make test # would run the fork's Makefile with your secrets
The checkout alone executes nothing. The next step does: make test, npm install, a test runner or a build configuration file all run attacker-controlled code with your token and secrets. GitHub's docs call this a pwn request and say the pattern has caused multiple supply-chain compromises. The GitHub Security Lab adds a point teams often miss: even with no secrets referenced, the job holds the read/write repository token, and actions/checkout persists credentials by default, so later steps can read the token from disk. Since v6 that credential sits in a file under $RUNNER_TEMP rather than .git/config, but it is still persisted by default.
GitHub lists three common vulnerable shapes:
- Checking out the pull request head or merge ref (
head.sha,refs/pull/<n>/merge) and then building or testing it.
- Pointing
repository:at the fork (github.event.pull_request.head.repo.full_name).
- Fetching pull request code another way, such as
git fetch,gh pr checkoutor an artifact from a fork's run, and then running it.
The problem is not unique to this trigger: an issue_comment or workflow_run workflow that fetches and runs fork code is exposed in the same way. Our guide to npm trusted publishing and cache-poisoning hardening shows how this chain plays out in a release pipeline.
Two related protections are already live:
- Checkout refuses fork code.
actions/checkoutv7 (18 June 2026) refuses to fetch fork pull request head or merge code inpull_request_targetworkflows, and inworkflow_runworkflows triggered by a pull request event. On 20 July 2026 GitHub backported the check to every supported major except v1; floating major tags such as@v4picked it up, but SHA, minor or patch pins did not. The opt-out input,allow-unsafe-pr-checkout: true, is named so reviewers and scanners notice it; treat it as a review flag.
- Read-only cache. Since 26 June 2026, untrusted triggers such as
pull_request_target,issue_commentand fork-drivenworkflow_runruns get read-only access to the default branch's cache, which closes a common cache-poisoning route.
Step 1: find every affected workflow
Do this before 16 November so the second brownout confirms your list.
Use policy insights first. GitHub tells owners to check insights to see which runs the rule would block. The Policy insights page sits under Actions policies in the repository, organisation or enterprise sidebar, and lists runs that were blocked or would have been blocked.
Check the 9 November brownout. Any public repository whose pull_request_target workflow fired between 16:00 and 18:00 UTC on 9 November should show a failed run with an error annotation. That is a real-world list of workflows that broke.
Search code across the organisation. In GitHub code search:
org:your-org path:.github/workflows pull_request_target
Sweep every public repository locally. Search results are not a complete inventory, so a shallow clone and grep is a dependable cross-check:
ORG=your-org
gh repo list "$ORG" --visibility public --no-archived --limit 2000 \
--json nameWithOwner -q '.[].nameWithOwner' > public-repos.txt
mkdir -p audit && cd audit
while read -r repo; do
gh repo clone "$repo" "$repo" -- --depth 1 --quiet 2>/dev/null || true
done < ../public-repos.txt
# 1. Every workflow that uses the trigger
grep -rl 'pull_request_target' */*/.github/workflows/ > prt-workflows.txt
# 2. The risky shapes inside them (fork code checked out or fetched)
xargs grep -nE 'head\.sha|head\.ref|refs/pull/|head\.repo\.full_name|gh pr checkout|git fetch|allow-unsafe-pr-checkout' \
< prt-workflows.txt
# 3. Other privileged triggers to review in the same pass
grep -rlE '^[[:space:]]*(workflow_run|issue_comment)[[:space:]]*:' */*/.github/workflows/
Turn on CodeQL for GitHub Actions. GitHub recommends it for finding common workflow vulnerabilities. It catches the patterns above and injection bugs that a grep will not.
Record each hit with four facts: repository, workflow file, whether it checks out or fetches pull request code, and which secrets or token permissions it uses. Those four columns decide the fix.
Step 2: triage each workflow
| What the workflow does | Uses fork code? | Needs secrets or write token? | Fix |
|---|---|---|---|
| Labels, triages or comments on PRs, no checkout of PR code | No | Write token only | Keep pull_request_target; allow it by a policy scoped to this workflow file; set least-privilege permissions |
| Builds and tests fork code, no secrets needed | Yes | No | Switch to pull_request |
| Builds fork code, then posts coverage or results with a token | Yes | Yes, after the build | Split: pull_request builds and uploads an artifact; workflow_run reads it as data and posts |
| Deploys a preview of fork changes | Yes | Yes | Split as above, and put the privileged job behind a protected environment with required reviewers |
| Runs on closed for post-merge tasks such as release notes or backports | Usually no | Yes | Move to push on the default branch, or allow by scoped policy if it never touches PR code |
| Uses a safe-to-test label to gate fork code with secrets | Yes | Yes | Replace; label gating is a temporary, race-prone fix |
| Checks out PR head with allow-unsafe-pr-checkout set to true | Yes | Yes | Prove the code is only read as data, never executed, or redesign |
The Security Lab treats label gating as a stopgap because an attacker can push new commits after the label is applied and before the run starts. The closed row catches people out: GitHub documents pull_request_target with the closed type as the way to act when a pull request merges, so those workflows are blocked too.
Step 3: replace the trigger with safe patterns
Pattern A: plain pull_request with least privilege
For most build and test workflows, this is the whole fix. pull_request runs the fork's code without your secrets and with a read-only token.
name: ci
on:
pull_request:
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7 # pin to a full commit SHA in production
with:
persist-credentials: false
- run: npm ci && npm test
Fork runs may need a maintainer to approve them, depending on your approval settings. GitHub asks maintainers to look closely at any change under .github/workflows/ before approving, and runs left waiting for approval for more than 30 days expire and are marked as failed.
Pattern B: pull_request plus workflow_run
When a result must be written back with a token, split the work. This is the GitHub Security Lab's recommended pattern, adapted with current action versions.
# .github/workflows/receive-pr.yml (unprivileged)
name: Receive PR
on:
pull_request:
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- run: ./build.sh && ./coverage.sh > coverage.txt
- run: |
mkdir -p out
echo "${{ github.event.number }}" > out/NR
cp coverage.txt out/
- uses: actions/upload-artifact@v7
with:
name: pr-result
path: out/
# .github/workflows/comment-pr.yml (privileged, runs from the default branch)
name: Comment on PR
on:
workflow_run:
workflows: ["Receive PR"]
types: [completed]
permissions:
actions: read
pull-requests: write
jobs:
comment:
runs-on: ubuntu-latest
if: >
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'success'
steps:
- uses: actions/download-artifact@v8
with:
name: pr-result
path: ${{ runner.temp }}/pr # never extract into the workspace
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/github-script@v9
with:
script: |
const fs = require('fs');
const n = Number(fs.readFileSync(`${process.env.RUNNER_TEMP}/pr/NR`, 'utf8').trim());
if (!Number.isInteger(n)) throw new Error('bad PR number');
const body = fs.readFileSync(`${process.env.RUNNER_TEMP}/pr/coverage.txt`, 'utf8').slice(0, 60000);
await github.rest.issues.createComment({ ...context.repo, issue_number: n, body });
Two rules make this safe. The artifact is attacker-influenced, so the privileged job only reads it as text and never runs a binary or script from it. And the artifact goes to runner.temp, not the workspace, so nothing in it can shadow a script a later step might call. The Security Lab also notes that the PR number in the artifact is attacker-controlled, which it considers acceptable for posting a comment and nothing more powerful.
Pattern C: protected environments for privileged jobs
If the privileged half needs real credentials, for example to publish a preview build to a sandbox bucket, put that job behind an environment:
jobs:
deploy-preview:
runs-on: ubuntu-latest
environment: fork-preview # required reviewers + prevent self-review
permissions:
contents: read
id-token: write # prefer OIDC over stored cloud keys
steps:
- run: echo "deploy the downloaded static files to an isolated preview target"
Environment secrets stay locked until one of the required reviewers approves the job. You can name up to six users or teams as reviewers, and the prevent self-review option stops the person who started the run from approving it. Plan limits matter: on GitHub Free, Pro and Team, required reviewers and wait timers work only in public repositories, which is exactly where this change applies. GitHub's pricing page lists environment protection rules for private repositories only on Enterprise. Our breakdown of GitHub Enterprise pricing with Copilot included covers that plan in detail.
Pattern D: keep pull_request_target for narrow jobs
Labelling and triage are what the trigger was built for. Keep them, but keep them small:
name: labeler
on:
pull_request_target:
types: [opened, synchronize, reopened]
permissions:
contents: read
pull-requests: write
jobs:
label:
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@<full-commit-sha> # no checkout of PR code anywhere
Step 4: allow what must stay with a scoped event policy
GitHub gives two options: keep the default block, or explicitly allow pull_request_target in an applicable Actions event policy, using workflow file targeting to allow only specific workflows. In the interface, open Settings, then Actions, then Policies at repository or organisation level; at enterprise level, open the Policies tab, then Actions, then Policies. There you name the policy, pick an enforcement status, target repositories and workflows, and add event rules. Execution protections cover all public repositories, so Free organisations can create these policies too.
The same thing through the Actions policies REST API:
gh api -X POST /orgs/your-org/actions/policies \
-H "X-GitHub-Api-Version: 2026-03-10" --input - <<'JSON'
{
"name": "Allow pull_request_target for labeler only",
"enforcement": "active",
"conditions": {
"repository_name": { "include": ["~ALL"], "exclude": [] },
"workflow_path": { "include": [".github/workflows/labeler.yml"], "exclude": [] }
},
"rules": [
{ "type": "restrict_action_events",
"parameters": { "allowed_events": ["pull_request_target"] } }
]
}
JSON
Three cautions before you run it:
- Event rules are an allowlist. The API field is
allowed_events, and GitHub describes execution protections as an allowlist of permitted events. For the workflows a policy targets, an event you leave out is not permitted. Iflabeler.ymlalso runs onworkflow_dispatch, list both or that trigger stops working.
- Scope by workflow path. Leaving out
workflow_pathtargets every workflow, so this policy would allowpull_request_targetfor every workflow in every targeted repository and, under the same allowlist logic, disallow every event you did not list.
- Evaluate mode needs Enterprise. The guide marks Evaluate as GitHub Enterprise Cloud only, and the API says the same. On Free and Team, test a new active policy on one repository first.
GitHub's docs do not say outright whether a policy scoped to one workflow file counts as applicable for the rest of a repository's workflows. Confirm the outcome in policy insights, or watch the 16 November brownout, before you rely on it.
An organisation-wide fix plan
| When (2026) | Owner | Action |
|---|---|---|
| 11 to 13 November | Platform team | Run the inventory: policy insights, 9 November failed runs, code search, local sweep. Build the four-column list |
| 13 to 15 November | Repo owners | Triage each workflow with the table above. Switch simple build and test jobs to pull_request |
| 15 November | Organisation owners | Create scoped allow policies for labelling and triage workflows only |
| 16 November, 16:00 to 18:00 UTC | Platform on-call | Watch the brownout. Every failure is either a missed workflow or a policy scoping mistake |
| 17 to 27 November | Repo owners | Build pull_request plus workflow_run splits and protected environments for the remaining jobs |
| 28 November to 1 December | Security | Review every allow-unsafe-pr-checkout, label gate and workflow_run consumer, and every SHA-pinned checkout older than the July backport |
| 2 December | Everyone | Enforcement. Watch for failed runs with error annotations and fix forward |
| December | Platform team | Add actor rules, require actions pinned to full commit SHAs, and adopt workflow lockfiles |
The SHA check in the 28 November row matters because the July checkout backport reached floating major tags only; a workflow pinned to an older commit still lacks the fork-checkout guard. One old worry no longer applies: because pull_request_target now always runs the workflow from the latest default-branch commit, a fix merged there also covers pull requests opened before it.
For the December row, actor rules let you separate who can contribute code from who can run CI, and they can name GitHub Apps, Copilot and Dependabot. The Require lockfile protection blocks targeted workflows that are not onboarded to the repository's lockfile. Workflow lockfiles became generally available on 28 October 2026 for GitHub Enterprise Cloud, Team and Free. Our software supply chain hardening guide covers SHA pinning and related controls. If your teams run coding agents that open pull requests, actor rules also belong in your AI coding agent rollout and governance plan.
Worked example: an organisation with 60 public repositories
This scenario is illustrative. The counts and hours are planning assumptions, not measurements; only the GitHub behaviours are verified.
An open-source SDK publisher has 60 public repositories. The inventory finds 14 workflows using pull_request_target across 11 repositories:
| Workflow group | Count | Fix | Assumed hours each | Hours |
|---|---|---|---|---|
| Labelling and triage bots, no PR checkout | 6 | Keep, allow through 1 scoped organisation policy | 0.5 | 3 |
| Build and test of fork code, no secrets | 4 | Switch to pull_request | 1 | 4 |
| Coverage comment needing a write token | 3 | Split into pull_request plus workflow_run | 4 | 12 |
| Preview deploy needing cloud credentials | 1 | Split, plus protected environment and OIDC | 6 | 6 |
| Total | 14 | 25 |
Arithmetic: (6 × 0.5) + (4 × 1) + (3 × 4) + (1 × 6) = 3 + 4 + 12 + 6 = 25 engineer-hours, or a little over three eight-hour days for one person (25 ÷ 8 = 3.1). The split adds no Actions charge: minutes on standard GitHub-hosted runners are free in public repositories, as is self-hosted runner usage, though larger runners are billed even there.
The six labelling workflows are the cheapest fix and the easiest to get wrong: one organisation policy with a workflow_path condition covers all six if they share a file name; if three repositories call it triage.yml, you need both paths in the include list.
India and Enterprise Server notes
India. The final brownout runs from 21:30 to 23:30 IST on Monday 16 November, outside most Indian working hours. Teams that maintain public SDKs or open-source tools for global customers should put someone on call for that window. Where a stolen secret could reach systems holding personal data, a leak is a data protection question too, so list which secrets a fork-triggered job can ever see.
GitHub Enterprise Server. GitHub says Enterprise Server is not covered by this change. The risk still applies there. Run the same inventory and apply the same patterns on your own schedule. Nothing in GitHub's notice varies by region.
How eCorpIT can help
eCorpIT is a Gurugram-based technology organisation, founded in 2021, assessed at CMMI Level 5 and MSME certified, with senior-led engineering teams working across AWS, Microsoft and Google platforms. Our cybersecurity services team can inventory your workflows, rebuild risky pull_request_target, workflow_run and issue_comment jobs, and write scoped Actions policies you manage through the API. Talk to us at /contact-us/.
Last updated: 11 November 2026.