Атаки на цепочку: typosquatting и компрометация пакетов

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

Атака на цепочку поставок — это внедрение вредоносного кода не напрямую в вашу систему, а в один из компонентов, которым вы доверяете: пакет, его обновление или реестр, откуда вы его берёте.

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

Зачем это знать защитнику

Установка пакета часто выполняет произвольный код (скрипты postinstall в npm, setup.py в Python) с правами разработчика или CI-раннера. Это значит, что вредоносный пакет получает доступ к вашим переменным окружения, токенам, исходникам — ещё до того, как вы запустите своё приложение. Понимание векторов помогает выстроить защиту на уровне политики, а не надеяться на удачу.

Как устроены такие атаки (на уровне принципа)

Typosquatting

Злоумышленник публикует пакет с именем, похожим на популярный, в расчёте на опечатку или невнимательность: reqeusts вместо requests, crossenv вместо cross-env, djagno вместо django. Разработчик, копируя команду из устаревшего гайда или просто промахнувшись по клавише, ставит подделку. Внутри — рабочая копия настоящей библиотеки плюс «нагрузка», которая, например, выгружает переменные окружения.

Dependency confusion (путаница реестров)

Многие компании используют внутренние пакеты с именами вроде acme-internal-utils, недоступные публично. Если менеджер пакетов настроен искать сначала в публичном реестре, злоумышленник может опубликовать в публичном npm/PyPI пакет с тем же именем и более высокой версией. Установщик решит, что это «более свежая» версия, и подтянет публичную подделку вместо внутренней.

Компрометация легитимного пакета

Самый опасный сценарий: атакуют не имя, а сам популярный пакет. Способы — угон аккаунта мейнтейнера (украденный или подобранный пароль, отсутствие 2FA), социальная инженерия (злоумышленник входит в доверие и получает права на публикацию), внедрение через зависимость самого пакета. Дальше выходит вредоносное обновление уже доверенной библиотеки, и оно автоматически прилетает всем, у кого версия не зафиксирована.

Как это работает под капотом

Ключевая механика — момент установки. Рассмотрим, что бывает в манифесте npm:

{
  "scripts": {
    "postinstall": "node ./setup.js"
  }
}

Скрипт postinstall выполняется автоматически при npm install. Для легитимного пакета он может, например, скомпилировать нативный модуль. Для вредоносного — это точка запуска нагрузки. Именно поэтому простое скачивание зависимости уже опасно: код исполняется до вашего ревью.

Вторая механика — алгоритм разрешения версий. Установщик по умолчанию стремится взять наибольшую подходящую версию из всех доступных источников. Без явного приоритета приватного реестра это и открывает dependency confusion: подделка с версией 99.0.0 «выигрывает» у внутренней 1.2.0.

Как защититься

Защита строится из нескольких слоёв — ни один не идеален в одиночку:

  • Проверяйте имя перед установкой. Прежде чем добавить пакет, откройте его страницу: сколько загрузок, как давно публикуется, кто мейнтейнер, есть ли репозиторий. Свежесозданный пакет с именем-омонимом популярного и нулём звёзд — красный флаг. Копируйте имена из официальной документации, а не из случайных статей.
  • Пиннинг версий + lock-файл. Фиксированная точная версия с хешем (через lock-файл) означает, что вредоносное обновление не прилетит само. Вы обновитесь только осознанно, прогнав тесты и проверки.
  • Скоупы и приватные реестры. Внутренние пакеты публикуйте под организационным скоупом (@acme/utils) и настройте менеджер так, чтобы скоуп @acme резолвился ТОЛЬКО из приватного реестра. Это закрывает dependency confusion.
# .npmrc — приватный скоуп идёт только во внутренний реестр
@acme:registry=https://nexus.internal.acme.com/repository/npm/
//nexus.internal.acme.com/repository/npm/:_authToken=${NPM_TOKEN}
  • Отключайте install-скрипты там, где можно. В CI запускайте установку с флагом, запрещающим произвольные скрипты (npm ci --ignore-scripts), а нужные нативные сборки разрешайте точечно. Это снижает риск автозапуска нагрузки.
  • Карантин и проверка обновлений. Не накатывайте мажорные/неожиданные обновления зависимостей мгновенно. Пусть бот откроет PR, а человек или сканер проверит дифф и репутацию версии. Многие атаки ловятся за часы — небольшая задержка спасает.
  • Включите 2FA на публикацию своих пакетов и используйте токены с минимальными правами. Это защищает уже ваших пользователей от угона вашего аккаунта.
  • Юридическое напоминание: публикация вредоносного пакета — это создание и распространение вредоносного ПО (ст. 273 УК РФ). Анализировать такие техники допустимо только в образовательных целях и на своих стендах; цель этого материала — защита, а не воспроизведение атак.

    Итоги

    • Вредоносный код попадает в проект через typosquatting (похожие имена), dependency confusion (путаница приватного и публичного реестра) и компрометацию легитимных пакетов.
    • Опасность в том, что установка пакета часто выполняет произвольный код (postinstall) ещё до запуска вашего приложения.
    • Защита многослойная: проверка имени, пиннинг версий с lock-файлом, приватные реестры и скоупы, отключение install-скриптов в CI, карантин обновлений.
    • 2FA на публикацию своих пакетов защищает их пользователей от угона вашего аккаунта.
    Проверьте себя
    1. В чём суть атаки dependency confusion?
    AЗлоумышленник публикует в публичном реестре пакет с именем внутреннего и более высокой версией, и установщик подтягивает подделку вместо приватного пакета
    BПакеты с похожими именами путают разработчика при наборе команды установки
    CДва пакета конфликтуют по версиям и приложение не собирается
    DРеестр случайно отдаёт чужой пакет из-за ошибки кеширования
    2. Почему даже простое скачивание вредоносной зависимости опасно ещё до запуска вашего приложения?
    AПотому что пакет сразу попадает в публичный реестр под вашим именем
    BПотому что установка часто выполняет произвольный код (например, скрипт postinstall) с правами разработчика или CI
    CПотому что lock-файл автоматически доверяет любому новому пакету
    DПотому что менеджер пакетов отключает антивирус на время установки