Command Palette

Search for a command to run...

Hectal
Lab 3.1·Pipeline & RuntimeCRITICAL

A CI trigger that trusts strangers

To label PRs from forks, a workflow uses pull_request_target and checks out the PR's code. That runs untrusted code with repository secrets and a write token.

Weakness
CICD-SEC-4 · Poisoned pipeline execution
Target
.github/workflows/pr-checks.yml in sec-lab
pull_request vs pull_request_targetzizmor dangerous-triggerssplitting privileged and unprivileged workflows

Run these techniques only against the lab you own. Using them on systems you don't have permission to test is illegal.

The threat

  1. 01Why this combination is dangerous

    pull_request runs a fork's PR with a READ-ONLY token and no secrets, which is safe by design. pull_request_target runs in the context of the BASE repository, with secrets and a write-capable token, so it can label or comment on fork PRs. That's safe only as long as it never executes the PR's code.

    Checking out the PR head and then running anything from it (npm install, a test script, a Makefile) executes code written by whoever opened the PR, with your secrets. Several well-known open-source projects have had exactly this misconfiguration reported and fixed.

    .github/workflows/pr-checks.yml (vulnerable)whole fileyaml
    on: pull_request_target
    jobs:
      test-and-label:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
            with:
              ref: ${{ github.event.pull_request.head.sha }}   # untrusted code
          - run: npm ci && npm test                          # runs it, with secrets
          - run: gh pr edit ${{ github.event.number }} --add-label tested
            env: { GH_TOKEN: "${{ secrets.BOT_TOKEN }}" }

What's at risk

  • Any GitHub user who opens a PR can run code with the workflow's secrets and write access to the repository.

Detect

  1. 01zizmor flags the trigger and the checkout

    terminal
    $ pipx run zizmor .github/workflows/pr-checks.yml
    ── output ──
    error[dangerous-triggers]: use of fundamentally insecure workflow trigger
    --> .github/workflows/pr-checks.yml:1:1
    |
    1 | on: pull_request_target
    | ^^^^^^^^^^^^^^^^^^^^^^^ pull_request_target is almost always used insecurely
    |
    = note: audit confidence → Medium
    1 finding: 0 unknown, 0 informational, 0 low, 0 medium, 1 high

Defend

  1. 01Split untrusted work from privileged work

    Run tests on pull_request: untrusted code, no secrets, read-only token. Do the privileged action (labelling) in a separate workflow triggered by workflow_run after tests complete, which never checks out or executes PR code and only reads the result.

    Vulnerable

    pr-checks.ymladd to fileyaml
    on: pull_request_target
    # checkout PR head + npm ci + npm test, with secrets

    Hardened

    pr-tests.yml + pr-label.ymlwhole fileyaml
    # pr-tests.yml — untrusted code, no secrets
    on: pull_request
    permissions: { contents: read }
    jobs:
      test:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@<sha> # v4
            with: { persist-credentials: false }
          - run: npm ci --ignore-scripts && npm test
    
    # pr-label.yml — privileged, never runs PR code
    on:
      workflow_run:
        workflows: ["pr-tests"]
        types: [completed]
    permissions: { pull-requests: write }
    jobs:
      label:
        if: github.event.workflow_run.conclusion == 'success'
        runs-on: ubuntu-latest
        steps:
          - run: gh pr edit "$PR" --repo "$REPO" --add-label tested
            env:
              GH_TOKEN: ${{ github.token }}
              REPO: ${{ github.repository }}
              PR: ${{ github.event.workflow_run.pull_requests[0].number }}
  2. 02Also require approval for first-time contributors

    Repository settings → Actions → 'Require approval for all outside collaborators' means workflows from forks don't run until a maintainer approves, which is another layer against drive-by PRs.

Verify

  1. 01No dangerous triggers remain

    terminal
    $ pipx run zizmor .github/workflows/
    ── output ──
    No findings to report. Good job!

The concepts

Poisoned pipeline execution

Any time a pipeline runs code or configuration that an untrusted party can modify (PR code, build scripts, workflow files, dependency install scripts) in a context holding secrets or write access, that party effectively controls the pipeline. The fix is always the same shape: untrusted input runs unprivileged, privileged steps run only trusted code.

Your turn

01

Besides checkout, what else in a workflow can let untrusted input run as code?

Interview questions

01

What's the risk of pull_request_target in GitHub Actions?

0/4 · 0%