> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pipefort.com/llms.txt
> Use this file to discover all available pages before exploring further.

# CICD-SEC-1 — pull_request_target trigger boundary confusion

> A workflow bound to both pull_request triggers, and privileged jobs that read attacker-controlled head context.

| Field      | Value                                                                                                                                                          |
| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Rule ID    | `cicd-sec-1-pr-target-dual-trigger`                                                                                                                            |
| Category   | `CICD-SEC-1`                                                                                                                                                   |
| Severity   | **HIGH** (head-context read) · MEDIUM (dual trigger)                                                                                                           |
| Confidence | HIGH                                                                                                                                                           |
| Platforms  | GitHub Actions                                                                                                                                                 |
| OWASP      | [CICD-SEC-1: Insufficient Flow Control Mechanisms](https://owasp.org/www-project-top-10-ci-cd-security-risks/CICD-SEC-01-Insufficient-Flow-Control-Mechanisms) |
| Auto-fix   | partial (trigger block, comment only)                                                                                                                          |

## What the check does

Two findings.

**MEDIUM — the workflow is bound to both `pull_request` and
`pull_request_target`.** Anchored on the `pull_request_target` entry, since that
is the one to remove.

**HIGH — a `pull_request_target` or `workflow_run` job reads
`github.event.…head.…`.** Both spellings are matched: the `pull_request` event's
nested `head` object, and `workflow_run`'s flattened `head_sha` / `head_ref` /
`head_branch` / `head_commit` / `head_repository`. One finding per job.

## Why it matters

The trigger names differ by one word and the security models are opposites:

| Trigger               | Runs                                      | Token     | Secrets |
| --------------------- | ----------------------------------------- | --------- | ------- |
| `pull_request`        | the fork's code                           | read-only | none    |
| `pull_request_target` | the **base branch's** workflow definition | write     | full    |

Bound to **both**, every job runs twice under two different models, and a reader
has to hold both in their head to reason about any one job. The usual history is
someone adding `pull_request_target` to make secrets available and forgetting to
remove the original — at which point the privileged copy is the one an attacker
targets, and nobody is looking at it.

The second finding is the bridge itself. `pull_request_target` runs with
repository secrets and a write-scoped token; `github.event.pull_request.head.*`
is content the pull-request author controls. Where the two meet is the classic
**pwn-request**:

```yaml theme={null}
on: pull_request_target                                    # ← secrets + write token
jobs:
  build:
    if: github.event.pull_request.head.repo.fork == false  # ← attacker-controlled
    steps:
      - uses: some/action@v1
        with:
          branch: ${{ github.event.pull_request.head.ref }}
```

This is deliberately broader than
[CICD-SEC-1 dangerous checkout](/rules/cicd-sec-1), which fires only on
`actions/checkout` with an untrusted `ref:`. Head content reaches a job through
an `if:` guard, another action's `with:` input, or a job `env:` just as well —
and none of those are checkouts.

## Safe alternative

**Keep one trigger.** Use `pull_request` for anything that builds or tests
contributor code — it gets no secrets, which is the point.

If a job genuinely needs secrets on a fork PR, move *just that job* into a
separate `pull_request_target` workflow that does not check out or execute head
content, and gate it on a label or an approving review:

```yaml theme={null}
on:
  pull_request_target:
    types: [labeled]

jobs:
  comment:
    if: github.event.label.name == 'safe-to-test'
    runs-on: ubuntu-latest
    steps:
      - run: gh pr comment "$NUMBER" --body "queued"   # ← no head content anywhere
        env:
          GH_TOKEN: ${{ github.token }}
          NUMBER: ${{ github.event.pull_request.number }}
```

Read the head ref only to post a comment or set a status — never to check out,
build, execute, or interpolate into a shell.

## What does not fire

* Either trigger **alone**. `pull_request_target` on its own is a deliberate
  choice, not a confusion; other rules cover what it then does.
* A head-context read under a plain `pull_request` — there are no secrets and no
  write token for it to reach.

## Auto-fix

**Partial, and scoped to the trigger block.** Dropping `pull_request_target`
changes which secrets the workflow can see; dropping `pull_request` changes when
it runs at all; splitting the file is a refactor, not a rewrite. Any of those
could silently break a release path, so the fix annotates the trigger that needs
the decision and leaves the decision to a human. Both triggers stay in place, and
running the fix twice does not stack a second comment.

The head-context finding gets **no** fix — there is nothing mechanical to do
about a job reading untrusted input.

## Related rules

* [CICD-SEC-1 — Dangerous checkout in `pull_request_target` / `workflow_run`](/rules/cicd-sec-1)
* [CICD-SEC-1 — `workflow_run` downloads artifacts from the triggering run](/rules/cicd-sec-1-workflow-run-artifact-poisoning)
* [CICD-SEC-4 — Reusable workflow called with `secrets: inherit` under a privileged trigger](/rules/cicd-sec-4-secrets-inherit-pr-target)
* [CICD-SEC-1 — Security decision based on a spoofable actor check](/rules/cicd-sec-1-spoofable-actor-condition)
