Dangerous trigger runs fork-controlled code with base-repo secrets
Control: Workflow must not use dangerous triggers· Config key: workflowMustNotUseDangerousTriggers
📋 What is this?
A job runs under a trigger that carries the base repository's secrets while being fireable by unprivileged users (workflow_run, issue_comment, pull_request_review, pull_request_review_comment, discussion, discussion_comment, gollum, fork) and checks out fork-controlled code: untrusted code then executes with the base repo's secrets and GITHUB_TOKEN. The trigger alone is never flagged, and a recognised job-level if: guard (same-repository check, workflow_run gated to an upstream push, trusted author_association allowlist) silences the finding. The pull_request_target case is owned by ISSUE-804.
⚠️ Impact
The same exploit class as the March 2025 tj-actions/changed-files compromise (CVE-2025-30066). Once fork code runs with the base repo's secrets, every shell step is an exfiltration path (npm install scripts, pytest conftest.py, even attacker-supplied file content); the blast radius is the union of the job's permissions and every secret the workflow can read.
🔧 How to fix
Three options, from strongest to most permissive: (1) drop the head checkout and let the job only operate on the base branch; (2) move the work to a plain pull_request trigger that runs without secret access; or (3) keep the head checkout but add a same-repository if: guard so fork-controlled runs never reach the job. Option 3 is the right answer for trusted-maintainer-only preview/build flows.
# .github/workflows/preview.yml: ❌ workflow_run chains off a fork-influenceable workflowon: workflow_run: workflows: ["lint"] types: [completed]jobs: preview: runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event.workflow_run.head_sha }} # ← fork-controlled - run: npm install && npm test # ← runs with base secrets# .github/workflows/preview.yml: ✅ same-repository guard: fork code never runson: workflow_run: workflows: ["lint"] types: [completed]jobs: preview: if: github.event.workflow_run.head_repository.full_name == github.repository runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event.workflow_run.head_sha }} - run: npm install && npm test
# .plumber.yamlgithub: controls: workflowMustNotUseDangerousTriggers: enabled: true💡 Tips
- The
pull_request_targetexploit pattern is owned by ISSUE-804 (pullRequestTargetMustNotCheckoutHead); this rule excludes it to avoid double-firing on the same job. - Recognised guards: same-repository (
head.repo.full_name == github.repository,head.repo.fork == false,head.repo.fork != true,!github.event.pull_request.head.repo.fork); aworkflow_runrestricted to an upstream push (github.event.workflow_run.event == 'push'); and a trustedauthor_associationallowlist (== 'OWNER' | 'MEMBER' | 'COLLABORATOR', orcontains(fromJSON('[…]'), …author_association)). A negated check likeauthor_association != 'OWNER'is a denylist, not a guard, and still fires. - Recognised untrusted refs:
github.event.pull_request.head.sha,head.ref,github.head_ref,github.event.workflow_run.head_sha,head_branch. - Metadata-only jobs (labelling, milestone, comment reactions) under these triggers are safe and intentionally not flagged.
- Pair with ISSUE-305 for the environment-gate side of the same risk surface.
⚙️ Configuration
This control is configured in .plumber.yaml under the github section:
github:
controls:
workflowMustNotUseDangerousTriggers:
enabled: trueSee the CLI documentation for the full configuration reference.