Skill 189 · GitHub Actions Hardening
Subchapter 189.5
references/triggers-and-privilege.mdMarkdown3 KBView on GitHub
The single most important question for workflow security is: can an outside contributor trigger this workflow, and if so, what token and secrets does it get? GitHub answers this differently per trigger.
| Trigger | Who can fire it | GITHUB_TOKEN | Secrets available | Risk |
|---|---|---|---|---|
push | Repo collaborators | read/write | yes | Low — trusted authors |
pull_request (same-repo branch) | Collaborators | read/write | yes | Low |
pull_request (from a fork) | Anyone | read-only | no | Low by design — even malicious code can’t steal anything |
pull_request_target | Anyone with a fork | read/write | yes | High — runs in base-repo context |
workflow_run | Fires after another workflow | read/write | yes | High |
issue_comment, issues | Anyone | read/write | yes | High |
The trap: pull_request from a fork is safe because GitHub deliberately strips the token down
and withholds secrets. Maintainers who find that “the secrets don’t work on fork PRs” often switch
to pull_request_target to get them back — and in doing so hand a write token and every secret to
arbitrary contributors.
pull_request_target checks out the base repository’s workflow definition (so a fork can’t
change what runs), but it runs with full privileges. The danger is when the workflow then
explicitly checks out the fork’s code and executes it:
# DANGEROUS — RCE with a write token + secrets
on: pull_request_target
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@<sha>
with:
ref: ${{ github.event.pull_request.head.sha }} # fork's code
- run: npm install && npm test # runs the fork's code + scriptsnpm install alone runs arbitrary lifecycle scripts from the PR. With pull_request_target those
scripts can read secrets.* and push commits with the write token.
Split responsibilities. An unprivileged workflow runs the untrusted code; a privileged workflow consumes only the trusted output.
# 1) Unprivileged: runs untrusted code, no secrets, read-only token
name: PR Build
on: pull_request
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@<sha>
- run: npm ci && npm run build
- uses: actions/upload-artifact@<sha>
with: { name: pr, path: dist/ }# 2) Privileged: triggered by the first, never runs fork code
name: PR Comment
on:
workflow_run:
workflows: ["PR Build"]
types: [completed]
permissions:
pull-requests: write
jobs:
comment:
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@<sha> # data only, not executed
# post results, using the trusted token — but never execute the artifactpull_request_target, workflow_run, issue_comment, and issues as privileged.pull_request (with its safe read-only/no-secrets defaults) whenever possible.