Skip to main content
ISSUE-802 Critical Medium CLI Workflow triggers and permissions

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 combines attacker-controlled input with the base repository's secrets — workflow_run, issue_comment, pull_request_review, pull_request_review_comment, discussion, discussion_comment, gollum, fork — **and checks out fork-controlled code** (an actions/checkout whose ref: is the workflow_run head or a PR-controlled ref). Untrusted code then executes with the base repo's secrets and GITHUB_TOKEN. The pull_request_target case is owned by [ISSUE-804](./ISSUE-804) (pullRequestTargetMustNotCheckoutHead), so this rule never fires on it — same exploit class, dedicated rule, no double-firing. Subscribing to such a trigger is **not** flagged on its own: labelling, milestone, comment and notification workflows legitimately need them and are safe without an untrusted checkout. The finding fires only on the exploitable combination, and abstains when a job-level if: neutralises the risk — a same-repository pull-request guard, a workflow_run gated to an upstream push (github.event.workflow_run.event == 'push'), or a trusted author_association allowlist.

⚠️ Impact

This is the same exploit class as the March 2025 tj-actions/changed-files vector (CVE-2025-30066) that exfiltrated secrets from hundreds of projects including aquasecurity/trivy. Once fork code runs with the base repo's secrets, every shell step is a direct exfiltration path — npm install (runs package scripts), pytest (loads conftest.py), even cat README.md (via attacker-supplied 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.

✗ Before The fork-controlled head SHA from the chained workflow runs with the secret-bearing GITHUB_TOKEN — same exploit class as the tj-actions / CVE-2025-30066 vector.
# .github/workflows/preview.yml — ❌ workflow_run chains off a fork-influenceable workflow
on:
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
✓ After The `if:` blocks fork-originated runs entirely; only same-repository commits ever reach the checkout.
# .github/workflows/preview.yml — ✅ same-repository guard: fork code never runs
on:
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.yaml
github:
controls:
workflowMustNotUseDangerousTriggers:
enabled: true

💡 Tips

  • The pull_request_target exploit 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); a workflow_run restricted to an upstream push (github.event.workflow_run.event == 'push'); and a trusted author_association allowlist (== 'OWNER' | 'MEMBER' | 'COLLABORATOR', or contains(fromJSON('[…]'), …author_association)). A negated check like author_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: true

See the CLI documentation for the full configuration reference.