Евгений Чернышев

Техлид в «Долгосрочной аренде»

Что может быть привычнее для разработчика, чем ежедневный ввод команд установки или обновления зависимостей: npm install, pip install, uv add, bundle add, cargo add? Пакетный менеджер находит нужную библиотеку, скачивает её вместе с транзитивными зависимостями и за считанные секунды устанавливает в проект сотни тысяч строк чужого кода. В этот момент мало кто задумывается о том, что совершает серьёзный акт доверия. Мы доверяем автору пакета, его аккаунту, системе публикации, сборочному конвейеру и всем зависимостям, которые подтягиваются вместе с ним. Бесспорно, большую часть этого кода мы не читали и, скорее всего, никогда не прочитаем. Пока всё работает, как задумано, этот процесс остаётся незаметным. Но стоит злоумышленнику скомпрометировать хотя бы одно звено в этой цепочке, и обычное добавление или обновление библиотеки превращается в установку вредоносного кода в сотни или даже тысячи проектов.

Именно на этой идее основана атака на цепочку поставок (Supply Chain Attacks). Злоумышленнику зачастую не нужно искать уязвимости в вашем приложении, планировать дорогостоящие многовекторные атаки и применять социальную инженерию. Достаточно увести учётную запись разработчика даже малоизвестного пакета, добавить бэкдор в новую версию и опубликовать её в тот же NPM или PyPi. Вуаля — вредоносный код начинает отправлять персональные данные ваших клиентов прямо на серверы хакеров.

Действительно ли проблема стоит так остро

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

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

  • Event Stream: в обновление транзитивной зависимости этой библиотеки добавили обфусцированный вредоносный код. Более 8 млн раз из npm загружалась труднообнаруживаемая «зараза», которая похищала крупные суммы криптовалюты у небольшой части пользователей.

  • Shai-Hulud: этот «червь» обитает не на далёком Арракисе, и охотится он вовсе не на краулеры, а на ключи к GitHub и NPM, заражая пакеты на компьютере разработчика и самостоятельно публикуя себя в реестре библиотек экосистемы JavaScript. По разным оценкам, осенью 2025 г. было заражено от 150 до 500 пакетов.

  • Рубишный Rest Client: интересный вектор заражения Rails-приложений, когда вредоносный код загружался с Pastebin и запускался через механизм Eval на устаревшей минорной версии этой библиотеки (1.6) при попытке обновить патчевую версию. Неприятный факт: зловред молчал в Dev-окружении, чтобы дольше оставаться незамеченным.

  • Torchtriton в Python-экосистеме: этот пакет, от которого зависели Nightly-сборки под Linux популярнейшего PyTorch, распространялся из отдельного реестра зависимостей (не PyPi). Примечательно то, что сам PyTorch не был взломан — злоумышленник просто опубликовал пакет c вредоносным кодом с тем же именем в публичном PyPi, и pip автоматически установил зависимость из центрального Python-реестра.

  • Faster_log и async_println в Cargo: здесь был использован схожий с привычным нам фишингом механизм — имена зловредных библиотек не только мимикрировали под настоящие fast_log и async-println, но и скопировали их документацию и API. Разработчик при беглом просмотре Cargo.toml ни за что не заметил бы подмены. Эта «зараза» запускалась исключительно в Runtime, а стандартные проверки при установке зависимостей и сборке ничего не выявляли. Но при запуске собранной программы приватные ключи Solana и Ethereum, хранящиеся на локальной машине разработчика, оказывались в руках злоумышленников.

Некоторые экосистемы популярных языков программирования менее подвержены этой проблеме (например, из-за отсутствия Install-скриптов в Go), но абсолютной защиты всё равно не существует, так как корень этой проблемы — доверие.

Где начало и где конец

Отдельная зависимость не существует «в вакууме». Прежде чем попасть в промышленную эксплуатацию (Production) конечного приложения, она проходит через множество этапов:

Злоумышленники могут атаковать каждый из них:

  • Украсть GitHub-аккаунт разработчика библиотеки

  • Похитить токен публикации и выложить другой артефакт, который не будет соответствовать исходному коду на GitHub

  • Опубликовать в реестре зависимость с похожим названием (Typosquatting)

  • Выполнить вредоносный код через Post-Install скрипт

  • Дождаться запуска вредоносной логики уже во время работы конечного приложения

Показательный пример — атака на Python-библиотеку компьютерного зрения Ultralytics. В этом случае не взламывали PyPi и не компрометировали аккаунты разработчиков на GitHub. Взлом произошёл через кеш GitHub Actions. В результате в центральный реестр Python-пакетов попали заражённые версии, опубликованные от имени официального разработчика.

Где тонко, там и рвётся: цепочка поставок безопасна ровно настолько, насколько надёжно её самое слабое звено.

Сверим хеши загружаемых пакетов

Идея лежит на поверхности. Наверное, в этом есть смысл. У разных файлов (оригинального и подменённого зловредом), конечно же, значения хеш-сумм будут различаться. Вероятность коллизии (два разных файла будут иметь одинаковые хеши) у того же SHA-256 крайне мала, и это даёт нам определённую уверенность. При наличии Lock-файла, в котором для каждой зависимости посчитан хеш SHA-256, незаметно подменить пакет практически невозможно.

Однако проблема лежит в другой плоскости. Lock-файл и контрольные суммы гарантируют лишь то, что мы устанавливаем в проект именно ту зависимость, которую устанавливали изначально. Но они не могут гарантировать, что она изначально была безопасной. Получается, что если в проект добавили зависимость, содержащую вредоносный код, то она будет устанавливаться постоянно, пока малварь не обнаружат и пакет не отзовут из того же PyPi.

Проверим исходный код

На первый взгляд это кажется разумным. Разработчику полезно изучать исходный код популярных библиотек. Однако на практике мы обычно заглядываем на GitHub лишь для того, чтобы прочитать README, возможно, чтобы просмотреть последние влитые PR, количество Issues и звёздочек. А затем смело пишем в терминале npm install some_awesome_package, берём кружку и идём к кофе-машине.

Важно понимать, что пакетный менеджер устанавливает не сам репозиторий с кодом, а опубликованный артефакт. (Опустим здесь возможность того же Bundler установить пакет напрямую с GitHub, если это явно указано.) Между исходным кодом и артефактом, размещённым в публичном хранилище пакетов, есть этапы сборки и публикации.

К Supply Chain Attack готов
К Supply Chain Attack готов

Злоумышленники всегда могут надеть балаклаву и толстовку с капюшоном, сесть за компьютер с тремя мониторами (разумеется, на каждом открыт терминал), чтобы попытаться украсть у автора библиотеки API-токен и загрузить в npmjs/pypi/packagist небольшой «сюрприз». Обнадёживает то, что экосистемы пакетов популярных языков программирования в последнее время начали усложнять жизнь «ребятам в толстовках и балаклавах», внедряя некоторые механизмы защиты. Одним из важных шагов вперёд стал механизм Trusted Publishing/OIDC.

Классическая публикация пакета устроена довольно просто. Меинтейнер создаёт в PyPI, NPM или RybyGems API-токен со смыслом: «Тот, у кого есть эта строка, имеет право публиковать новые версии пакета». Этот токен кладут в секреты GitHub и хранят там годами. Это напоминает кражу ключа от квартиры, замки в которой никогда не меняются: украл сегодня — можешь вернуться через месяц, если владелец не заметил пропажу. При этом способов украсть такой токен множество: скомпрометированный GitHub Actions, случайно выведенные Secrets в журнале, вредоносная зависимость, взлом аккаунта разработчика.

Trusted Publishing предлагает вообще не хранить такой секрет. Например, для PyPI можно задать ограничение: «Пакет awesome_super_python_package разрешено публиковать только из репозитория super_company/awesome_super_package из конкретного GitHub Actions Workflow». При запуске Workflow GitHub выдаёт ему подписанный токен OIDC (OpenID Connect), который выступает чем-то вроде временного удостоверения личности: «Я действительно GitHub Action, выполняющий Workflow публикации пакета awesome_super_python_package из репозитория super_company/awesome_super_package». PyPI проверяет подпись OIDC-токена и создаёт короткоживущий (на момент написания статьи — 15 мин.) API-токен публикации. Постоянный токен хранить больше не нужно. Аналогичный механизм есть и у RubyGems, а для NPM такой подход считается предпочтительным уже около года. Но есть важная оговорка: этот механизм подтверждает личность публикующего процесса, но не гарантирует безопасность того, что он публикует. Если злоумышленник каким-то образом скомпрометирует GitHub Actions Workflow, то PyPI воспримет его как доверенный Workflow и, ничего не подозревая, опубликует заражённый пакет.

Модель защиты можно развивать и дальше. Например, попробовать самому собрать артефакт из исходников и проверить хеш-суммы (Reproducible Builds) или подписывать информацию о происхождении артефакта: как он был собран, когда, из каких исходников и т.д. (Attestations).

Ой, сломалось
Ой, сломалось

Все эти проверки не доказывают, что код безопасен. Разработчик библиотеки может сам добавить бэкдор, репозиторий могут взломать, а доверенный CI-конвейер — скомпрометировать. Но атакующему становится гораздо сложнее подменить одно звено и при этом сохранить видимость, что цепочка поставок осталась прежней. В последнее время менеджеры пакетов и репозитории переходят от модели «скачал и доверился» к модели «скачал и получил набор доказательств происхождения».

Ставим Nexus и спим спокойно

Наиболее очевидный корпоративный подход — полностью запретить прямую загрузку пакетов из публичных репозиториев. Кажется, что теперь заражённые зависимости не попадут на локальные машины, в CI/CD-конвейер и даже в промышленный контур. Идея хорошая, но нельзя забывать про некоторые нюансы.

Если Nexus работает в режиме прокси для тех же pypi.org или registry.npmjs.org, он может загрузить скомпрометированный пакет и навсегда сохранить его в корпоративном контуре. В некотором смысле ситуация может оказаться даже хуже, чем при использовании публичных хранилищ: при обнаружении и подтверждении вредоносного кода в каком-либо NPM-пакете команда NPM Security обычно удаляет его в течение суток. А в Nexus, если не принять дополнительных мер, такой пакет останется доступным.

Гораздо более зрелый подход — когда команда кибербезопасности жёстко настраивает правила маршрутизации пакетов в Nexus или полностью запрещает установку зависимостей из внешнего интернета для CI/CD. Разработчику становится сложнее случайно запустить вредоносный код в Production- или CI-окружении. Хорошей практикой считается настройка политик, по которым новые пакеты и их версии попадают в корпоративный репозиторий. Например, можно перед загрузкой проверять пакеты по базам известных CVE и не допускать их публикацию в Nexus.

Считаю своим долгом указать, что даже самые строгие политики безопасности, Сooldown и статический анализ кода не дают гарантии безопасности каждой зависимости. Злоумышленник может очень хорошо припрятать вредоносную логику глубоко в коде. Представим, что вышла новая версия пакета awesome-helper 1.7.34. Она не вызывает подозрений:

  • Нет известных CVE

  • Нет сигнатур Malware

  • Репозиторий существует

  • Автор настоящий

  • Лицензия MIT

  • Нет назойливых Protestware

Nexus и инструменты, основанные на политике публикации, спокойно пропускают такой пакет из внешнего PyPI или NPM. Однако во время сборки приложения может выполниться «сюрприз» из awesome-helper@1.7.34:

import random
import os

number = random.randint(0, 10)

is_ci = os.environ.get('CI') == 'true'
connection_string = os.environ.get('DB_CONNECTION_STRING')

if number == 5 and is_ci and connection_string:
    clients_personal_data = fetch_personal_data_from_db(connection_string)
    send_to_scammers(clients_personal_data)

Пусть корпоративный репозиторий пакетов — это не полная защита от Supply Chain Attacks, но он создаёт достаточно прочный барьер. Nexus позволяет перейти от модели «Я слепо доверяю чему угодно, что лежит в интернете» к модели «Я контролирую зависимости, которыми пользуется моя компания».

У этого проекта на GitHub 50 тыс. звёздочек. Переживать не стоит

Интуиция подсказывает: если библиотека популярна, имеет много звёзд на GitHub и миллионы установок на npmjs.org, значит, она априори безопасна. Действительно, если библиотека скачивается десять раз в неделю, то это выглядит подозрительно, а если 10 млн раз, то, скорее всего, ей можно доверять. Такую библиотеку сопровождают высококвалифицированные специалисты, тысячи разработчиков ежедневно изучают её код, а опыт промышленной эксплуатации накоплен годами. Даже если «зараза» и появится, её быстро обнаружат. Звучит убедительно, но это скорее миф (Many Eyes), чем реальность. Показательные примеры, такие как Heartbleed и атака на xz, в своё время буквально сотрясли индустрию.

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

Здесь как раз показателен приведённый выше пример с event-stream. Пакет был хорошо известен в JS-сообществе и имел миллионы загрузок. Проблема возникла, когда основной разработчик устал поддерживать библиотеку и передал права человеку, предложившему помощь. Новый владелец добавил новую транзитивную зависимость flatmap-stream, через которую позже был внедрён вредоносный код. В итоге ещё вчера event-stream был популярным и проверенным пакетом, а сегодня — инструментом для кражи криптовалюты на кошельки хакеров.

Таким образом, количество установок и звёзд на GitHub лишь отражает популярность пакета, но ничего не говорит о том, кто выпустит следующую версию. При выборе пакета важно учитывать не только его популярность, но и другие факторы: жив ли проект, сколько у него активных меинтейнеров, как часто менялись владельцы, есть ли Install-скрипты, используется ли Trusted Publishing и т.д.

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

Как появился этот пакет. Транзитивные зависимости — код, который никто не выбирал

По моему опыту, в проекте команда обычно использует около 30—40 прямых зависимостей. Однако размер и содержимое папки node_modules давно стали мемом. Почему так происходит и какие риски это несёт для информационной безопасности?

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

$ npm install some-library

Но проблема в том, что мы почти никогда не устанавливаем только требуемую зависимость. Как правило, some-library сама зависит от других библиотек. Выглядит это примерно так:

Цепочка зависимостей
Цепочка зависимостей

Мы выбрали some-library , автор some-library добавил в проект helper-a, разработчик parser-btiny-util. Про tiny-util из нашей команды никто даже не слышал, но её код всё равно оказывается у нас в проекте. Если в package.json у нас указано несколько десятков зависимостей, то в package-lock.json их может быть сотни или даже тысячи. И каждый такой пакет — потенциальная точка входа для Supply Chain Attack.

Именно такая ситуация и произошла с event-stream: вредоносный flatmap-stream никто сознательно не устанавливал. Для пользователей всё выглядело вполне обычно:

Supply Chain Attack доехала, никто не заметил
Supply Chain Attack доехала, никто не заметил

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

В итоге получается довольно странная модель доверия: «Мы доверяем не только разработчикам библиотек, которые выбрали сами, но и всем, кому доверяют они, и тем, кому доверяют те разработчики». Поэтому фраза «Мы используем только проверенные зависимости» в таком контексте теряет смысл. Мы можем вручную проверить, скажем, десять зависимостей, но если вместе с ними в проект попадут ещё 700, то поверхность атаки становится огромной.

Конечно, это не значит, что нужно полностью отказаться от транзитивных зависимостей. Без них современная разработка была бы непрактичной и экономически невыгодной. Важно понимать цену удобства: если awesome-helper экономит разработчику 20 строк кода, но при этом тянет за собой 15 зависимостей и пару-тройку Post-Install-скриптов, то лучше от такой библиотеки отказаться.

Ой, попался
Ой, попался

Поэтому при выборе библиотеки стоит обращать внимание не только на неё саму, но и на то, что она приносит с собой. Количество библиотек — это не просто вопрос размера папки node_modules, а размер вашей цепочки доверия.

Обновляться сразу или немного подождать

С зависимостями возникает неприятная дилемма. С одной стороны, новая версия пакета может содержать обновления безопасности, новые фичи, важный bugfix или поддержку новой версии языка. С другой — вместе с обновлением может прийти «подарочек». Интуиция подсказывает выбирать между двумя крайностями: обновляться сразу или не обновляться вообще. Обе стратегии проигрышные.

Если обновляться сразу после каждого релиза, мы фактически становимся бесплатными бета-тестерами не только багов, но и Supply Chain Attack. Злоумышленники рассчитывают именно на это: публикуют вредоносную версию и пытаются проникнуть в максимальное число проектов, пока проблему не обнаружили. Например, заражённые версии популярных пакетов chalk и debug находились в NPM Registry всего пару часов — для человека это мало, но для автоматического бота, который увидел новый релиз, открыл PR и автоматически его влил, — вполне достаточно.

Отсюда и появилась идея Cooldown: не брать новую версию пакета сразу после публикации, а дать ей пару-тройку дней «отлежаться». Например, Dependabot по умолчанию ждёт три дня перед созданием PR для обычного обновления версии (обновления безопасности при этом не задерживаются). Похожую функциональность недавно (начиная с версии 4.0.13) добавили в рубишный Bundler:

source "https://rubygems.org", cooldown: 7

В этой конфигурации новая версия gem-а должна провести в RubyGems минимум семь дней, чтобы Bundler начал её рассматривать при разрешении зависимостей. Суть не в том, что через семь дней пакет автоматически станет безопасным, — всё гораздо проще:

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

Применяя Cooldown, мы просто стараемся не быть первыми в «очереди на раздачу».

Стратегия «вообще ничего не обновлять» — ещё хуже. Старые зависимости постепенно накапливают известные CVE, перестают поддерживаться и начинают конфликтовать с новым окружением. В итоге мы рискуем погрязнуть в техдолге, и обновление с версии 2.7 до актуальной 7.4 может занять несколько месяцев разработки.

Как однажды сказал Оби-Ван Кеноби: «Только ситхи всё возводят в абсолют!» Для нас разумная стратегия лежит посередине: обновляться регулярно, но не сразу после выхода новых версий. Разумеется, если речь идёт не о критических обновлениях безопасности.

Где опаснее всего

Когда говорят о вредоносной зависимости, прежде всего думают о Production-окружении: пакет попал в приложение, запустился на сервере и начал делать что-то очень плохое. Но для злоумышленников гораздо интереснее CI. Они прекрасно знают, что Production-окружение зрелого проекта обычно максимально изолировано. А CI/CD-конвейер по определению должен уметь многое:

  • Читать исходный код проекта

  • Собирать приложение

  • Забирать приватные зависимости

  • Развёртывать

  • Получать секреты

То есть CI/CD — это место, где сходятся код, инфраструктура и учётные данные. Если вредоносная зависимость выполняется именно там, атакующему даже не нужно проникать в Production. На этапе npm install кажется, что ничего важного ещё не произошло. Однако процесс имеет доступ к таким переменным окружения, как:

  • AWS_ACCESS_KEY_ID

  • AWS_SECRET_ACCESS_KEY

  • DOCKER_PASSWORD

  • GITHUB_TOKEN

  • AMQP_DSN

  • DB_CONNECTION_STRING

Если зависимость умеет выполнять код во время установки, ей достаточно просто прочитать переменные окружения и отправить их значения «наружу». Поэтому Install-скрипты так привлекательны для Supply Chain Attacks, ведь код запускается ещё до того, как разработчик начал использовать библиотеку.

Примерно по такому принципу была построена атака Shai-Hulud. Вредоносные версии пакетов искали токены публикации NPM, учётные данные GitHub и ключи к облачным сервисам, чтобы заражать другие пакеты и распространять «червя». В этом случае CI становился не просто жертвой, а следующим звеном распространения атаки.

Один из главных принципов безопасности довольно прост: не давать зависимости на стадии установки больше прав, чем ей действительно нужно. В контексте защиты от атак на цепочки поставок нельзя воспринимать CI просто как машину для запуска тестов. Это привилегированная среда, в которой регулярно запускается чужой код. Поэтому npm install, bundle install, pip install или cargo build в CI стоит воспринимать с той же осторожностью, что и запуск неизвестного бинарника на сервере с доступом к Production.

Что делать, если уже вляпались

Самая неприятная особенность Supply Chain Attacks заключается в том, что о них обычно узнают не в момент заражения, а спустя часы или даже дни. Представим, что сегодня команда обновила зависимость some-awesome-library до версии 2.4.7. CI прошёл успешно, приложение запустили в промышленную эксплуатацию, команда пошла пить кофе — все довольны проделанной работой. Однако завтра появляется Advisory:

Версия 2.4.7 some-awesome-library была скомпрометирована.

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

  • Переменным окружения

  • SSH-ключам

  • Kubernetes-конфигурации

  • Ключам доступа к облачным провайдерам

  • Содержимому файловой системы

  • Доступным внутренним сервисам и многому другому

Поэтому после подтверждения факта заражения стоит исходить из неприятного предположения: всё, к чему заражённый процесс имел доступ, потенциально скомпрометировано. Если вредоносный код уже выполнился, например в CI, то недостаточно просто удалить пакет из package-lock.json. Нужно оценить ситуацию шире: где этот пакет запускался, к каким секретам имел доступ, какие сети и сервисы были доступны и что злоумышленник мог сделать, получив украденные данные.

Сначала необходимо остановить дальнейшее заражение. Для этого приостановите автоматические обновления, сборки и публикации артефактов, использующих заражённую зависимость. Также сразу удалите пакет из Lock-файла.

Затем определите «радиус поражения». Нужно понять, где именно выполнялась заражённая версия зависимости. Например, вредоносный код присутствует в 15 репозиториях, но выполниться успел только в одной CI-сборке. Или же заражена никому не известная транзитивная зависимость и пострадали сотни внутренних репозиториев и CI/CD-конвейеров. Для оценки проверьте:

  • Прямые и транзитивные зависимости

  • Lock-файлы

  • Журналы в CI

  • Образы Docker

  • Собранные внутренние библиотеки

  • Локальные окружения разработчиков

Будем честны: все секреты, к которым мог получить доступ заражённый процесс, следует считать украденными. Здесь начинается неприятная, но необходимая часть реагирования на инцидент: все секреты (токены публикации, SSH-ключи, доступы к облачным провайдерам и т.д.), до которых могла дотянуться «зараза», должны быть отозваны. Если украденный токен мог что-то опубликовать, обязательно проверьте историю публикаций. Если, например, «ушедший налево» AWS-токен позволял создавать новые учётные записи, необходимо проверить IAM. Простая ротация пароля в этом случае не спасёт.

Естественно, нужно удалить все артефакты — собранные пакеты, Docker-образы и т.д. Но ещё важнее не забыть про кеши: даже если заражённый пакет удалён из NPM или PyPI, он всё равно может оставаться в наших ~/.npm, pip cache, bundle cache, Nexus, Docker layer cache, CI cache. В итоге может получиться так, что команда вроде бы избавилась от скомпрометированной версии зависимости, но через неделю какой-то старый конвейер достаёт заражённый артефакт из кеша, и проблема возвращается.

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

Вывод из этого довольно простой и неприятный: если вредоносный пакет уже успел выполниться, недостаточно просто удалить зависимость. Нужно считать скомпрометированным всё окружение, к которому имел доступ заражённый код, и расследовать инцидент именно с этой точки зрения.

А за чей счёт банкет

С точки зрения защиты от атак на цепочки поставок есть ещё один неприятный аспект, о котором часто забывают. Представьте популярную библиотеку, которую используют 500 тыс. проектов, тысячи небольших компаний, а также десятки банков и облачных сервисов. При этом поддерживает её один человек, который занимается этим по вечерам после основной работы. От него ожидают, как от Геркулеса, целый список подвигов:

  • Проверять Pull Requests

  • Следить за зависимостями

  • Обновлять CI/CD-конвейеры

  • Настраивать двухфакторную аутентификацию

  • Внедрять Trusted Publishing

  • Регулярно просматривать Security Reports

  • Своевременно выпускать патчи

  • Не выгорать

И всё это желательно делать «за спасибо». С точки зрения бизнеса это очень выгодно, ведь простая команда npm install awesome-lib экономит команде месяцы, а то и годы разработки. Однако стоимость разработки этого кода никуда не исчезла — её просто оплатил кто-то другой.

Этот момент хорошо иллюстрирует описанный выше пример атаки на event-stream. В 2018 г. это был довольно популярный пакет, но основной разработчик перестал им пользоваться и не хотел его поддерживать. Когда появился человек, готовый взять проект в свои руки, передача прав выглядела логичной и оправданной: «Есть популярный проект, на который у меня нет уже ни сил, ни времени. Есть человек, который готов им заниматься. Почему бы не передать ему доступ?». Позже к транзитивным зависимостям «прилип» flatmap-stream, и у части пользователей начала пропадать криптовалюта.

Постфактум легко сказать: «Нужно было тщательнее проверять нового меинтейнера». Но кто именно должен был это делать? Человек, который годами бесплатно поддерживал библиотеку с миллионами установок? Компании, которые годами её использовали? NPM? Сообщество? Большим компаниям стоит поддерживать разработчиков Open Source через те же GitHub Sponsors, особенно если их инфраструктура критически зависит от этих проектов.

Бесплатная зависимость всё равно стоит денег. Да, лицензия MIT обходится компании в 0 руб. 0 коп., но это далеко не вся стоимость. Настоящая цена использования зависимости выглядит примерно так:

Стоимость зависимости
Стоимость зависимости

Если за это не платит тот, кто использует библиотеку, значит, кто-то выполняет всю работу бесплатно, а чаще всего она просто не выполняется. Это особенно опасно. Заброшенная библиотека не перестаёт работать: она продолжает жить, иметь сотни тысяч загрузок и десятки тысяч зависимых проектов. Для рядового разработчика это не вызывает подозрений, но любой специалист по кибербезопасности прекрасно понимает: Publish-токен такой библиотеки становится настоящим «золотым руном» для киберпреступников. И они наверняка приложат все усилия, чтобы им завладеть.

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

Есть ли вообще защита

Полностью защититься от Supply Chain Attacks невозможно. Теоретически можно отказаться от сторонних библиотек и написать всё самостоятельно. Но на практике это значит, что вместе с бизнес-логикой придётся поддерживать свой Awesome-драйвер к базе данных, Super-парсер JSON, Mega-Power-библиотеку криптографии и ещё 5-6 млн строк кода.

Можно пойти дальше: продать квартиру, машину, пару внутренних органов, купить на эти деньги токены у OpenAI или Anthropic и поручить Клоду или Кодексу переписать весь Open Source с нуля. Но и тогда остаётся вопрос: можем ли мы доверять самим моделям, обвязке агентов, контейнерам, компиляторам, базовым Docker-образам? Цепочка поставок никуда не исчезает — она просто становится длиннее и дороже.

В реальности выбор стоит не между «использовать зависимости или быть в безопасности», а между «слепо доверять зависимостям или управлять этим доверием».

Например, нельзя гарантировать что популярный пакет завтра не будет скомпрометирован. Но мы можем не спешить устанавливать его сразу после публикации новой версии в NPM, PyPI, RubyGems или crates.io. Мы не можем быть уверены, что Install-скрипт не содержит вредоносного кода, но можем не запускать его CI или делать это в изолированной песочнице без Production-секретов. Нельзя утверждать, что каждый меинтейнер никогда не потеряет доступ к своему GitHub-аккаунту, но можно использовать Lock-файлы, фиксировать версии и контролировать изменения.

Защита от Supply Chain Attacks — это не непробиваемая бетонная стена, а несколько закрытых дверей, стоящих одна за другой. Одну из них можно вскрыть, но незаметно преодолеть все сразу — намного сложнее.

Подведём итог: разные меры защиты решают разные задачи. Lock-файл сам по себе не проверяет, есть ли в пакете вредоносный код, он лишь не позволяет незаметно подменить уже зафиксированную версию. Cooldown не гарантирует обнаружение malware, но даёт время на это. Корпоративный Nexus не делает пакет автоматически безопасным, он лишь блокирует явно заражённые версии. Абсолютной безопасности не существует, но есть большая разница между тем, чтобы сразу загружать свежие версии зависимостей и запускать в CI/CD с Production-секретами и делать это в изолированной среде.

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