Codecov bash uploader взломан: проверяем CI/CD после очередной supply-chain атаки
Скомпрометированный bash-скрипт Codecov воровал переменные окружения из CI/CD. Проверяем свои пайплайны и переходим на pin версий с проверкой SHA256.
Codecov bash uploader скомпрометирован: вредоносный код в официальном скрипте загрузки покрытия собирал переменные окружения CI/CD и отправлял их на сторонний сервер
В начале апреля Codecov сообщил, что их официальный bash-скрипт для загрузки отчётов покрытия кода был подменён. Кто-то получил доступ к их инфраструктуре через скомпрометированные credentials от Docker Hub и внедрил в скрипт дополнительную строчку - она собирала переменные окружения CI/CD-воркера и отправляла их на сторонний IP. Это висело незамеченным примерно два месяца - с 31 января по 1 апреля.
Нас это задело напрямую: несколько наших пайплайнов используют Codecov.
Что именно произошло
Вредоносная строка в скрипте выглядела невинно - она просто добавляла в curl-запрос к серверу Codecov содержимое git remote -v и весь вывод env. Всё окружение CI/CD - токены, ключи, credentials от реестров, AWS-ключи, всё что лежит в переменных пайплайна - улетало на сторону.
Вектор элегантный и неприятный. Скрипт подтягивается командой вроде:
bash <(curl -s https://codecov.io/bash)
Это паттерн, который встречается в документации множества сервисов - Codecov, Homebrew, различные установщики. Удобно: одна строчка, всё установится само. Неудобно: вы запускаете произвольный код из интернета с правами текущего пользователя, без какой-либо проверки что именно пришло.
В данном случае скрипт был официальным. Просто в определённый момент он перестал быть тем, чем должен был быть.
Первичный разбор у нас
Когда Codecov опубликовал advisory, первое что мы сделали - посмотрели на свои пайплайны. Несколько проектов используют Codecov для отчётности по тестовому покрытию. Вопрос был простой: какая именно команда стоит в .gitlab-ci.yml?
Оказалось разное. Один проект тянул скрипт напрямую через curl без пиннинга - классический вариант из документации. Другой использовал pinned версию с указанием конкретного тега, но без проверки хеша. Третий вообще не использовал bash uploader - только Python-пакет. Это лотерея.
Для всех проектов с curl-вариантом мы прошлись по переменным окружения CI/CD: что там лежит, что могло утечь в период январь-апрель. В нескольких местах нашлись токены с избыточными правами - не потому что их украли, а потому что так сложилось исторически. Хорошая возможность разобраться.
Токены, которые потенциально могли быть в окружении в период компрометации, ротировали. Перестраховка, но в данной ситуации оправданная.
Корень проблемы: curl | bash без верификации
Скомпрометированный скрипт Codecov - это хорошая иллюстрация проблемы, которую мы сами у себя не разбирали системно: внешние скрипты в CI без верификации целостности.
bash <(curl -s https://example.com/install.sh) удобен. Но у него нет механизма проверки что вы получили именно то, что ожидали. DNS может быть скомпрометирован, CDN может быть взломан, сам сервис может быть скомпрометирован - как в случае с Codecov. Вы узнаете об этом только после того, как код уже выполнился.
Правильный подход тут два варианта, и лучше использовать оба вместе:
Первое - pin конкретной версии. Вместо того чтобы тянуть latest или просто URL без версии, указываем конкретный коммит или тег. Для Codecov это выглядит так:
curl -Os https://uploader.codecov.io/v0.1.13/linux/codecov
Конкретная версия, конкретный бинарь. Он не изменится задним числом - в отличие от curl https://codecov.io/bash, который отдаёт текущую версию скрипта каждый раз.
Второе - проверка SHA256. Перед запуском любого внешнего скрипта или бинаря - проверяем хеш:
curl -Os https://uploader.codecov.io/v0.1.13/linux/codecov
curl -Os https://uploader.codecov.io/v0.1.13/linux/codecov.SHA256SUM
shasum -a 256 -c codecov.SHA256SUM
chmod +x codecov
./codecov
Если скрипт был подменён - хеш не совпадёт, шаг CI упадёт с ошибкой. Это именно то, что должно было случиться с Codecov-скриптом - только у большинства этой проверки не было.
Что мы поменяли в пайплайнах
По всем проектам прошлись и вынесли установку внешних инструментов в отдельный шаг с явной верификацией. Для Codecov перешли на их новый uploader (они его переписали на Go именно после этого инцидента) с пиннингом версии и проверкой хеша.
Параллельно зафиксировали для себя правило: любой внешний скрипт или бинарь в CI - только через pinned версию и SHA256. Без исключений. Это немного дольше при первоначальной настройке и требует обновлять хеш при апгрейде версии - но это сознательный выбор, а не автоматическое доверие.
Отдельно посмотрели на минимизацию переменных окружения. Если job загружает покрытие в Codecov - ему нужен только Codecov-токен. Не AWS-ключи, не токен от registry, не SSH-ключи от серверов. Принцип минимальных привилегий применим не только к пользователям, но и к отдельным шагам пайплайна. CI/CD с широким доступом ко всем credentials сразу - это большая поверхность атаки.
Три supply-chain инцидента за четыре месяца
SolarWinds в декабре, атаки через зависимости npm и PyPI как фоновый шум, теперь Codecov. Атака через цепочку поставок - это уже не редкий сценарий из отчётов APT-исследователей, это практика.
Неприятная особенность Codecov-случая в том, что атака была направлена прицельно на CI/CD - место, где концентрируется доступ ко всему: коду, артефактам, облакам, серверам. Пайплайн с широкими правами и подтягиванием внешних скриптов - хорошая цель.
Конкретные шаги по аудиту CI/CD, которые мы сейчас проходим по клиентским проектам в рамках аудита безопасности: инвентаризация внешних скриптов и бинарей в пайплайнах, аудит переменных окружения на избыточность прав, проверка что критичные credentials не доступны всем job'ам подряд. Не революция - просто то, о чём нужно думать системно, а не после следующего инцидента.