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

Dependency confusion в npm и PyPI: аудит private-пакетов у трёх клиентов и настройка Nexus

После волны dependency confusion атак 2021 года проверяем имена private-пакетов трёх клиентов на конфликт с публичными реестрами и настраиваем Nexus с explicit allow-list.

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

Исследователи массово публикуют методику dependency confusion - захват внутренних пакетов через публичные реестры npm/PyPI, волна атак затронула крупные компании включая Microsoft и Apple

В феврале 2021 года исследователь Алекс Бирсан опубликовал детальный разбор атаки, которую он назвал dependency confusion. Суть простая и оттого неприятная: если ваша сборочная среда ищет пакеты сначала в публичном реестре, а потом во внутреннем, достаточно зарегистрировать в npm или PyPI пакет с тем же именем, что у вашего внутреннего - и при следующей сборке он будет скачан и выполнен вместо нужного. Бирсан провёл это против реальных компаний в рамках bug bounty - Microsoft, Apple, PayPal, Netflix - и получил выплаты. Публикация разошлась быстро, и с тех пор волна повторений от разных охотников за уязвимостями не прекращается.

Мы прочитали разбор в феврале и сразу подумали: а что у наших клиентов с именами пакетов? Мысль зафиксировали, но до дела дошло только сейчас - когда в октябре снова пошли публикации с новыми случаями. Взяли трёх клиентов с npm и Python-зависимостями и провели проверку.

Как работает атака

Механика зависит от конфигурации менеджера пакетов. По умолчанию npm при установке пакета обращается к публичному реестру registry.npmjs.org. Если организация использует внутренний scope (@company/package) - это относительно безопасно: scoped-пакеты ищутся в реестре, привязанном к scope, и подменить их через публичный npmjs сложнее. Но если внутренние пакеты называются без scope - просто utils, api-client, config-helpers - они технически существуют в том же пространстве имён, что и публичные пакеты.

Ситуация с pip аналогичная. Если используется корпоративный PyPI-прокси с --extra-index-url, pip при разрешении зависимостей смотрит в оба реестра и берёт версию с наибольшим номером. Исследователи научились публиковать в публичный PyPI пакет с версией 9999.0.0 - и pip радостно тянет его вместо внутреннего 1.2.3.

Что нашли у клиентов

У всех трёх клиентов картина оказалась похожей, хотя и в разной степени запущенности.

Первое - незащищённые имена в npm. У одного клиента внутренние пакеты назывались без scope: logging-utils, http-middleware, ещё пара похожих названий. Ни один из них не был зарегистрирован в публичном npmjs - то есть любой мог занять это имя прямо сейчас. Мы проверили через npm view <имя> - пакетов нет, позиция свободна. Это и хорошо (пока не атакованы), и плохо (ничего не мешает).

Второе - --extra-index-url без приоритетов. У другого клиента pip-конфиг выглядел невинно: корпоративный Artifactory как дополнительный индекс, публичный PyPI как основной. Именно та конфигурация, которую Бирсан описывал как уязвимую. Несколько внутренних пакетов с короткими именами - мы проверили на PyPI, часть имён занята посторонними пакетами с нулевой активностью. Это уже интереснее: занято, но не обязательно вредоносно. Тем не менее вектор открыт.

Третье - отсутствие проверки целостности. У третьего клиента package-lock.json не всегда коммитился в репозиторий. Это отдельная история, не про dependency confusion напрямую, но в связке с ней - если нет зафиксированных хешей, замечить подмену пакета сложнее.

Что настроили: Nexus с explicit allow-list

Самый надёжный способ закрыть вектор - контролировать, какие имена пакетов вообще могут прийти из публичного реестра. Для этого подходит Nexus Repository Manager с настройкой проксирующих репозиториев.

Схема, которую мы применили:

npm install ---> Nexus (hosted group)
                    |
                    +-- internal (hosted) <- ваши пакеты
                    |
                    +-- npm-proxy <- npmjs.org ТОЛЬКО по allow-list

В Nexus группа репозиториев выдаёт внутренние пакеты из hosted-репозитория с приоритетом над проксирующим. Это уже снижает риск, но не убирает его полностью - если имя пакета есть и внутри, и в npm, Nexus отдаёт внутренний. Но если во внутреннем нет - идёт в прокси.

Дополнительно мы настроили content selectors и routing rules в Nexus, чтобы запросы на конкретные имена внутренних пакетов не уходили во внешний npm вообще. По сути - explicit allow-list наоборот: эти имена доступны только из internal, в прокси не проваливаются.

Для PyPI аналогичная история: уходим от --extra-index-url в сторону единственного --index-url, указывающего на Nexus, который уже сам решает, откуда брать пакет. --extra-index-url с двумя реестрами - конфигурация, которую надо убирать.

Параллельно зарегистрировали занятые имена в публичных реестрах от имени клиента - как defensive registration. Это не защита сама по себе, но убирает возможность для атакующего занять имя. Бирсан, кстати, именно так и работал в своём исследовании: находил свободные имена, публиковал пустышки и ждал, что прилетит в его сторону.

Что ещё проверили

Несколько наблюдений по ходу работы, которые вышли за рамки исходной задачи.

Scoped-пакеты в npm (@company/...) действительно снижают риск dependency confusion, если scope привязан к приватному реестру. Но это не абсолютная защита: если scope не настроен явно в .npmrc или в Nexus - npm может попробовать его через публичный реестр. Проверили конфиги - в одном месте scope был объявлен, но не закреплён за конкретным реестром в .npmrc. Поправили.

Логирование в Nexus пригодилось. По access-логам мы увидели несколько запросов к именам, которые не должны были уходить в прокси. Один из них оказался опечаткой в requirements.txt, другой - легаси-зависимостью, про которую все забыли. Без логов не нашли бы.

Где сейчас

На двух клиентах из трёх изменения внедрены - Nexus перенастроен, package-lock.json зафиксирован в репозитории, defensive registration сделана. У третьего клиента процесс ещё идёт: там инфраструктура сложнее, несколько сборочных окружений с разными конфигурациями pip, и согласование правил займёт время.

Тема supply chain уже который месяц не уходит из повестки. После атаки на Kaseya и разговоров про SBOM dependency confusion выглядит логичным продолжением: атакующие ищут точку, в которой доверие встроено по умолчанию. Сборочная среда, которая автоматически тянет зависимости из публичного реестра, - именно такая точка.

Если вы используете внутренние пакеты без scope в npm или --extra-index-url в pip - проверьте имена через npm view и поиск на PyPI. Это занимает полчаса и даёт ответ на вопрос, есть ли у вас открытая позиция прямо сейчас. Аудит зависимостей - не разовое мероприятие, но начинать надо с чего-то конкретного.

Контакт

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

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