Learn (Almost) Anything
КаталогСкачать курс

Автоматизация в GitHub

Почему `pull_request_target` опасен при checkout кода из PR, как разделять read-only validation и privileged automation, approvals from forks и безопасные паттерны.

ruЭнциклопедия33 урока6 модулей9
GitHub actionsАвтоматизация в GitHubМодель workflowсостояние и артефактыматрицы и runtimeрелизы и environmentsПереиспользование и собственные actionsgovernance и эксплуатацияgithubactions

pull_request_target и untrusted PR

Почему `pull_request_target` опасен при checkout кода из PR, как разделять read-only validation и privileged automation, approvals from forks и безопасные паттерны.

pull_request_target и untrusted PR

pull_request_target — полезный, но security-sensitive trigger. Он запускает workflow на событие pull request, но делает это в контексте base repository: workflow-файл берётся из доверенной ветки репозитория, а не из PR. Поэтому GitHub разрешает сценарии вроде «поставить label», «оставить comment», «обновить metadata PR» даже для PR из fork. Цена этой возможности — повышенный риск: если в таком workflow выполнить код из PR, untrusted contributor может получить доступ к тому, что было предназначено только для доверенного workflow.

Короткое правило: pull_request — для build/test кода из PR; pull_request_target — для privileged automation вокруг PR, но не для запуска PR-кода. Это продолжение модели угроз из Secure use и threat model: событие PR приносит данные от внешнего автора, а workflow остаётся исполняемым кодом с token, network и side effects.

flowchart TD A[PR из fork] --> B{Что нужно сделать?} B -->|Собрать или протестировать PR-код| C[pull_request] C --> D[contents: read] D --> E[checkout PR-кода] E --> F[npm ci / tests / reports] B -->|Label, comment, triage metadata| G[pull_request_target] G --> H[явные permissions] H --> I[не checkout'ить head PR] I --> J[GitHub API: comment, label, reviewer] B -->|Privileged deploy или release| K[после merge / protected environment] K --> L[environment approval + OIDC] style C fill:#e8f5e9,stroke:#2e7d32 style G fill:#fff8e1,stroke:#f9a825 style K fill:#e3f2fd,stroke:#1565c0
flowchart TD
  A[PR из fork] --> B{Что нужно сделать?}
  B -->|Собрать или протестировать PR-код| C[pull_request]
  C --> D[contents: read]
  D --> E[checkout PR-кода]
  E --> F[npm ci / tests / reports]
  B -->|Label, comment, triage metadata| G[pull_request_target]
  G --> H[явные permissions]
  H --> I[не checkout'ить head PR]
  I --> J[GitHub API: comment, label, reviewer]
  B -->|Privileged deploy или release| K[после merge / protected environment]
  K --> L[environment approval + OIDC]
  style C fill:#e8f5e9,stroke:#2e7d32
  style G fill:#fff8e1,stroke:#f9a825
  style K fill:#e3f2fd,stroke:#1565c0
Разделяйте запуск untrusted PR-кода и privileged automation: разные triggers, разные permissions, разные границы доверия.

Где именно появляется опасность

В обычном pull_request workflow GitHub ограничивает fork PR: secrets не передаются, а GITHUB_TOKEN для fork PR имеет read-only права. Это хорошо подходит для CI: checkout, install, test, reports. Подробности такой механики лежат рядом с CI pipeline, Artifacts и reports и Permissions и GITHUB_TOKEN.

pull_request_target устроен иначе. Он выполняется как workflow base repository и может получить secrets или write-capable token, если вы не сузили permissions. Сам по себе trigger не «дырка»: он нужен, чтобы доверенный workflow реагировал на PR. Дырка появляется, когда workflow явно checkout’ит head PR и затем запускает install/build/test/script из этого checkout.

Классический опасный пример:

name: dangerous-pr-build
on: pull_request_target

permissions:
  contents: write
  pull-requests: write

jobs:
  build:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v6
        with:
          ref: ${{ github.event.pull_request.head.sha }}
          repository: ${{ github.event.pull_request.head.repo.full_name }}
      - run: npm ci
      - run: npm test
      - run: gh pr comment ${{ github.event.pull_request.number }} --body "Tests passed"
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Здесь автор PR контролирует package.json, lifecycle scripts, test scripts, build config и любые локальные actions внутри PR. Если npm ci запускает postinstall, этот код уже выполняется в privileged job. Даже если вы не передали секрет явно, action или shell-команда может увидеть доступный token, credentials в git config, environment variables, workspace, cache и artifacts.

Разделение: validation отдельно, automation отдельно

Production-паттерн обычно состоит из двух workflow.

Первый — read-only validation. Он запускает PR-код, но не имеет write-доступа и production secrets:

name: pr-validation
on:
  pull_request:
    types: [opened, synchronize, reopened]

permissions:
  contents: read

jobs:
  test:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v6
        with:
          node-version: 24
          cache: npm
      - run: npm ci
      - run: npm test

Второй — privileged automation. Он может писать comment или label, но не checkout’ит и не выполняет код из PR:

name: pr-triage
on:
  pull_request_target:
    types: [opened, reopened, labeled]

permissions:
  contents: read
  pull-requests: write
  issues: write

jobs:
  comment:
    runs-on: ubuntu-24.04
    steps:
      - name: Comment from trusted workflow
        run: |
          gh pr comment "$PR_NUMBER" --body "Спасибо за PR. CI запустится в read-only workflow."
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          PR_NUMBER: ${{ github.event.pull_request.number }}

Обратите внимание на границу: privileged workflow использует metadata PR — номер, labels, author, base branch, но не запускает код из head. Если нужно читать diff, лучше использовать GitHub API или пассивно сравнивать файлы. «Пассивно» значит: не выполнять npm install, make, локальные scripts, test runner, formatter из PR и локальные actions из PR.

Что насчёт checkout только для diff

Иногда pull_request_target всё же checkout’ит PR: например, чтобы посчитать changed files, проверить наличие changelog или сгенерировать diff. Это не автоматически уязвимость, но требует жёсткого режима:

- name: Passive file inspection only
  env:
    PR_NUMBER: ${{ github.event.pull_request.number }}
    GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  run: |
    gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files" --paginate --jq '.[].filename' | sed -n '1,200p'

Даже такой вариант лучше держать без secrets, с contents: read, без cache write и без запуска инструментов из репозитория. persist-credentials: false важен, потому что checkout по умолчанию может сохранить token для дальнейших git-команд. Но это не превращает workflow в безопасный build: любой шаг, который интерпретирует файлы PR как программу, снова пересекает границу доверия.

Approvals from forks не спасают pull_request_target

GitHub умеет требовать approval перед запуском workflow для PR из public fork. Maintainer с write-доступом смотрит изменения, особенно .github/workflows/, и нажимает «Approve workflows to run». Это полезно против resource abuse и случайного запуска workflow от незнакомого contributor.

Но approval policy не является защитой для pull_request_target: такие workflow запускаются в контексте base branch и поэтому могут стартовать независимо от approval settings. Если вы думали «мы включили approval for first-time contributors, значит privileged workflow безопасен», это неверная модель. Approval помогает обычным pull_request runs; для pull_request_target безопасность должна быть заложена в YAML: permissions, отсутствие PR-code execution, аккуратная работа с contexts и secrets.

Есть ещё один edge case: label вроде safe-to-test как ручной gate. Его можно использовать как временную страховку, но не как финальную архитектуру. После label автор PR может успеть push’нуть новые commits до старта workflow, если вы не pin’ите конкретный reviewed SHA. Надёжнее запускать тесты через pull_request без привилегий, а privileged действие делать отдельно после review или после merge.

Безопасные сценарии для pull_request_target

pull_request_target уместен, когда job делает действие от имени base repository, но не исполняет PR-код:

  • добавить label по author association, path metadata или PR title;
  • написать onboarding comment;
  • проверить, заполнен ли PR template, через GitHub API;
  • назначить reviewer или milestone;
  • закрыть PR по явно нарушающему policy условию;
  • реагировать на merged PR через types: [closed] и if: github.event.pull_request.merged == true.

Если задача звучит как «собрать», «протестировать», «запустить formatter», «опубликовать preview из кода PR», «прочитать package scripts», берите pull_request и минимальные права. Если после успешной проверки нужно privileged действие, используйте отдельный workflow и очень осторожно передавайте только проверенные данные. Для deploy-preview лучше привязать секреты к Environments и protection rules, а облачные credentials — к OIDC и секреты облаков, с условиями на branch, environment и trusted actor.

Практический audit-чеклист

Проверяя существующий repo, ищите такие красные флаги:

on: pull_request_target
# плюс что-то из этого:
- uses: actions/checkout@...
  with:
    ref: ${{ github.event.pull_request.head.sha }}
- run: npm ci
- run: pip install -r requirements.txt
- run: make test
- uses: ./.github/actions/some-local-action
permissions: write-all
permissions:
  contents: write

Не каждый checkout опасен одинаково, но сочетание pull_request_target + head checkout + execution step почти всегда требует redesign. Начните с простого: перенесите build/test в pull_request, поставьте permissions: contents: read, а pull_request_target оставьте только для comment/label. Затем проверьте Marketplace actions и pinning, потому что third-party action в privileged workflow получает ту же зону доступа, что и ваши shell steps.

See also

Внешние справки: GitHub Docs по pull_request_target, approvals for fork workflows, secure use reference и статья GitHub Security Lab «Preventing pwn requests».

Быстрое повторение #rc-1

Почему workflow на pull_request_target становится опасным, если он делает checkout commit’а из PR и затем запускает npm ci или тесты?

Показать ответ

Код из PR контролирует package.json, lifecycle scripts, тесты, build config и локальные actions. Он начинает выполняться внутри privileged job base repo, где могут быть write-token, secrets, git credentials, cache или artifacts.

Источники

  1. Events that trigger workflows - GitHub Docs
  2. Approving workflow runs from forks - GitHub Docs
  3. Keeping your GitHub Actions and workflows secure Part 1: Preventing pwn requests - GitHub Security Lab
  4. Secure use reference - GitHub Docs
  5. Disabling or limiting GitHub Actions for your organization - GitHub Docs