ADG Оставить заявку
Блог Информационная безопасность 5 мин чтения

Supply chain через npm и PyPI: пинируем зависимости по хешу и подключаем Snyk в CI

Атаки через публичные пакетные репозитории учащаются. Вводим pin dependencies по хешу, npm shrinkwrap, Dependabot alerts и Snyk для транзитивных зависимостей в CI.

Контекст момента

Учащаются атаки на цепочку поставок через npm и PyPI: вредоносные пакеты с typosquatting и подменой легитимных

В этом году npm и PyPI несколько раз оказывались в заголовках по одному и тому же поводу: кто-то публикует пакет с именем, похожим на популярный, или взламывает аккаунт мейнтейнера, и тысячи проектов тянут к себе код, который никто не проверял. Имена пакетов-двойников порой отличаются одним символом - классический typosquatting, на который в момент npm install никто не смотрит.

Мы дошли до этого не абстрактно. У одного из клиентов в ходе аудита обнаружили, что несколько внутренних Node.js-проектов тянут зависимости без lock-файлов в репозитории. CI скачивал последние совместимые версии на каждый билд. То есть сборка в понедельник и сборка в среду могли принести разный код - и никто не заметил бы разницы без специального инструмента.

Что конкретно происходило в 2020-м

Инцидентов несколько, и они разного характера:

  • Typosquatting - пакеты с именами вроде crossenv (вместо cross-env) или mongose (вместо mongoose). Публикуются в расчёте на опечатку при вводе или случайный install с ошибкой.
  • Подмена через публичный реестр - атака на среды, где есть внутренние пакетные реестры. Публичный пакет с тем же именем и более высокой версией «выигрывает» при разрешении зависимостей и подтягивается вместо внутреннего.
  • Account takeover - взлом аккаунта мейнтейнера и публикация новой «патч-версии» с payload. Сюда же - передача заброшенного пакета новому владельцу, который оказывается злоумышленником.

По последнему типу атак показательна история с event-stream в 2018-м - тогда npm-экосистема впервые поняла масштаб проблемы. В 2020-м она не исчезла, а стала более системной.

Что мы изменили у себя и у клиентов

Политику сформулировали коротко: ни одна зависимость не должна разрешаться в момент сборки без явной фиксации. Под этим - несколько конкретных шагов.

npm shrinkwrap и lock-файлы в репозитории. Звучит банально, но lock-файлы часто добавлены в .gitignore по привычке или по умолчанию из старых .gitignore-шаблонов. Первым делом проверили, что package-lock.json или npm-shrinkwrap.json коммитится и не игнорируется. npm shrinkwrap фиксирует дерево зависимостей включая транзитивные - это отдельно важно, потому что прямые зависимости обычно на виду, а транзитивные - нет.

Фиксация по integrity hash. В package-lock.json версии 2 каждый пакет имеет поле integrity с SHA-512 хешем tarballa. При npm ci (в отличие от npm install) npm проверяет этот хеш и падает, если он не совпадает. Именно npm ci нужен в CI-пайплайне - он не обновляет lock-файл молча, а валидирует его.

# В GitHub Actions - принципиальная разница
- run: npm ci         # проверяет integrity, не трогает lock
# вместо
- run: npm install    # может обновить lock тихо

Dependabot alerts на GitHub. Включили для всех репозиториев, где есть package-lock.json или requirements.txt. Это базовый уровень: Dependabot смотрит на прямые зависимости и сигнализирует о CVE из GitHub Advisory Database. Alerts - это не PR, это уведомление. PR на автоматическое обновление мы решили не включать повсеместно - слишком много шума и риск сломать тестированную конфигурацию.

Snyk в CI для транзитивных зависимостей. Dependabot смотрит на прямые - Snyk смотрит глубже. Интегрировали в GitLab CI одной строкой через официальный образ:

snyk-scan:
  image: snyk/snyk:node
  script:
    - snyk test --severity-threshold=high --json > snyk-report.json || true
    - snyk test --severity-threshold=critical
  artifacts:
    paths:
      - snyk-report.json
  allow_failure: false  # critical - блокируем

Логика такая же, как мы делали со SAST: High - в отчёт и видимость, Critical - блокируем merge. Высокий уровень при первом прогоне на легаси-проектах дал неожиданное количество транзитивных зависимостей с известными CVE - половина из них приходилась на пакеты, которые в package.json вообще не упоминаются. Их тянули чужие зависимости.

Про PyPI: там похожая история

Всё то же самое, только инструменты другие. Для Python фиксируем через pip freeze > requirements.txt в зафиксированном виртуальном окружении и коммитим. В CI используем pip install --require-hashes, который требует явного указания хеша для каждого пакета в requirements.txt:

cryptography==3.1.1 \
    --hash=sha256:7b2d54e787a884f4...

Генерировать такие файлы удобнее через pip-compile из pip-tools - он сам считает хеши и расписывает транзитивные зависимости. Snyk для Python тоже работает, хотя покрытие базы данных по сравнению с npm-экосистемой немного уже.

Что не решили

Честно: подмену через публичный реестр для внутренних пакетов мы закрыли не везде. Там нужен настроенный приватный реестр с явным скопом для внутренних пакетов и правильная конфигурация .npmrc. Это отдельная работа, которая у части клиентов впереди.

Плюс подписи пакетов - npm сейчас не требует подписи от мейнтейнеров, и интегрированной проверки подписи в стандартном flow нет. Здесь пока полагаемся на хеши в lock-файлах и Snyk-сигнатуры по базам уязвимостей.

Пинирование по хешу и Snyk в CI - это не серебряная пуля, но это конкретная планка, которую мы подняли. Атака через typosquatting на зафиксированную сборку с проверкой integrity уже не пройдёт незамеченной.

Контакт

Нужна такая же инженерная работа?

Опишите задачу и контекст. Ответим в течение рабочего дня, при необходимости подпишем NDA.