awesome-everything EN
↑ Обратно к восхождению

Безопасность

Безопасность цепочки поставок: твоя реальная поверхность атаки — это зависимости

Суть Ты отгружаешь куда больше чужого кода, чем своего, поэтому современная поверхность атаки — это шаг установки. Lockfile, пины версий, provenance и приоритет реестра — защита от бэкдоров уровня xz и dependency confusion.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на junior-высоте — поверхность
◷ 16 min

Инженер Microsoft бенчмаркил PostgreSQL на тестовой машине в марте 2024, когда SSH-логины стали занимать 500ms вместо обычных ~100ms. Эти полсекунды были единственным видимым симптомом CVE-2024-3094: бэкдора, посаженного в xz-utils — библиотеку сжатия, лежащую под sshd на большинстве дистрибутивов Linux. Код был чистым. Никто не написал уязвимой строки. Атакующий потратил примерно два года, чтобы стать доверенным мейнтейнером малоизвестной зависимости, а затем протащил бэкдор через сборку. Поймали это по регрессии latency, а не на код-ревью.

Ты отгружаешь в основном чужой код

Современное веб-приложение — тонкий слой собственной логики поверх глубокого стека зависимостей. Свежий create-next-app или проект на NestJS тянет сотни пакетов, подавляющее большинство — транзитивные: их ставят твои зависимости, а не ты, и они не названы в твоём package.json. Честная формулировка для сеньора: большинство байтов, что ты отгружаешь в прод, написаны незнакомцами, не проаудированы никем в твоей команде и обновляются автоматически.

Это меняет, куда бьют атаки. Cross-site scripting и SQL-инъекции целятся в твой код. Атаки на цепочку поставок пропускают твой код целиком и целятся в шаг установки — момент, когда твоя машина или CI забирает пакет и запускает его install-скрипты с твоими кредами, токенами реестра и доступом к сети. Можно написать безупречный прикладной код и всё равно быть взломанным на npm install.

Как сработали реальные инциденты

Четыре задокументированные атаки размечают поверхность. Каждая обосновывает конкретную защиту.

Бэкдор xz-utils (CVE-2024-3094, 2024) был многолетней социально-инженерной кампанией. Контрибьютор под именем «Jia Tan» потратил ~2021–2024 на построение доверия в проекте xz — патчи, помощь, аккаунты-куклы, давившие на выгоревшего оригинального мейнтейнера, — пока не стал со-мейнтейнером. Затем он спрятал вредоносный код в обфусцированных бинарных тестовых файлах и подправленном скрипте сборки, так что бэкдор существовал только в выпущенном tarball, а не в читаемом исходнике в Git. Он целился в авторизацию sshd и дошёл до Fedora, Debian unstable и Kali, прежде чем 500ms-подсказка по latency его выдала. Урок: доверие к мейнтейнеру — не доверие к сборке.

Dependency confusion (Alex Birsan, 2021) был проще и страшнее. Многие пакетные менеджеры, когда ты просишь пакет, проверяют публичный реестр наравне с приватным — и предпочитают более высокий номер версии. Birsan собрал имена внутренних пакетов, утёкшие в публичных JS-бандлах, опубликовал пакеты с этими точными именами в npm/PyPI/RubyGems с версией 9.9.9 и стал ждать. Сборки в Apple, Microsoft, Tesla, Uber и 30+ других молча резолвили его публичный пакет вместо внутреннего и запускали его код. За доказательство он получил более $130 000 в bug bounty.

Typosquatting ставит на промах пальцем: пакет с именем lodahs или crossenv рядом с настоящими lodash или cross-env. Одна опечатка в Dockerfile — и ты установил пакет атакующего.

Скомпрометированные легитимные пакетыevent-stream (2018) и ua-parser-js (2021) — показывают третий путь: реальный, широко используемый пакет получает вредоносную версию. У event-stream появился новый «мейнтейнер», добавивший пейлоад под конкретный криптокошелёк; ua-parser-js с десятками миллионов загрузок в неделю был угнан на несколько часов, чтобы отгружать криптомайнер и кражу кредов.

АтакаМеханизмГлавная защита
Бэкдор xz-utilsДоверенный мейнтейнер отгружает бэкдор в артефакте сборки, не в исходникеProvenance сборки (SLSA), воспроизводимые сборки, подписанные артефакты
Dependency confusionПубличный пакет затеняет внутреннее имя более высокой версиейScoped-имена + приоритет реестра; резервируй внутренние имена публично
TyposquattingПохожее имя рядом с популярнымLockfile + ревью новых зависимостей; —ignore-scripts
Скомпрометированный пакетРеальный популярный пакет получает вредоносную версиюПины версий + integrity-хэши; отложенное принятие; audit

Базовый слой: lockfile, integrity и npm ci

Lockfile (package-lock.json, yarn.lock, pnpm-lock.yaml) пинит каждую зависимость — прямую и транзитивную — к точной версии и криптографическому integrity-хэшу (sha512-...). Версия останавливает тихий скачок на вредоносный релиз; хэш означает, что пакет, чьи байты изменились под тем же номером версии, провалит установку. Это твоя первая и самая дешёвая линия обороны.

Но lockfile защищает, только если ты его реально форсишь. npm install с радостью мутирует lockfile, чтобы удовлетворить ослабленный диапазон; npm ci делает обратное — ставит ровно lockfile и падает с ошибкой, если package.json и lock расходятся. Правило сеньора: npm ci в CI, никогда npm install. Добавь --ignore-scripts, чтобы остановить запуск произвольного postinstall-кода во время установки — именно так выполняется большинство пейлоадов на этапе установки. Пины точных версий (без ^-диапазонов) плюс короткая задержка принятия — не бери релиз в день его выхода — притупляют окно в стиле ua-parser-js, когда вредоносная версия живёт несколько часов, прежде чем её снимут.

Почему это работает

Почему хэши, а не только пины версий? Номер версии — это метка, которой управляет публикатор; байты — нет. Если атакующий перепубликует ту же версию с новым содержимым (или реестр скомпрометирован), пин только по версии её спокойно поставит. Integrity-хэш вычисляется из реального tarball, поэтому любое изменение — вредоносное или случайное — ломает установку громко, а не выполняется тихо.

Слой целостности: SBOM, provenance и подпись

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

SBOM (Software Bill of Materials) — это список ингредиентов: машиночитаемая опись каждого компонента и версии в сборке, в стандартном формате вроде CycloneDX или SPDX. Когда выйдет следующий CVE, SBOM ответит «затронуты ли мы?» за секунды, а не паническим grep по репозиториям. SLSA (Supply-chain Levels for Software Artifacts) — дополняющая половина: она сертифицирует, как собирали артефакт. Её прогрессивные уровни идут от «provenance существует» (L1) до укреплённой, изолированной, нефальсифицируемой сборки (L3); большинство команд целятся в L2–L3 для прода. Provenance — подписанная, проверяемая запись, связывающая артефакт с точным коммитом исходника и сборкой, что его произвела, чтобы подменённый бинарь не выдал себя за официальный. Подписанные коммиты и подписанные артефакты (через Sigstore / in-toto attestations) замыкают петлю: ты проверяешь подпись, прежде чем доверять байтам, а не доверяешь имени.

СлойНа какой вопрос отвечаетИнструменты
Lockfile + хэшПолучил ли я ровно те байты, что ожидал?npm ci, поле integrity
SBOMКакие компоненты в этой сборке?CycloneDX, SPDX, Syft
Provenance (SLSA)Как и из какого исходника собрано?SLSA attestations, in-toto
ПодписьАртефакт подлинный и неподменённый?Sigstore, подписанные коммиты

Остановить dependency confusion: приоритет, а не везение

Confusion-атаки эксплуатируют порядок резолва. Фикс — сделать так, чтобы сборка никогда не лезла в публичный реестр за внутренним именем. Используй scoped-неймспейс (@acme/billing), которым ты владеешь в публичном реестре, чтобы под ним никто не смог опубликоваться. Настрой пакетный менеджер так, чтобы у внутреннего/приватного реестра был приоритет для твоего scope, и либо проксируй публичные пакеты через один прокси, либо вовсе блокируй публичный реестр для внутренних scope. На всякий случай публикуй пакеты-заглушки под своими внутренними именами в публичный реестр сам — владеть именем проще всего, чтобы гарантировать: атакующий его не займёт. Автоматическое ревью зависимостей (Dependabot, npm audit, Renovate) затем следит за известными уязвимыми версиями и помечает рискованные новые зависимости прямо в PR, прежде чем они попадут в lockfile.

Выбери лучший вариант

Твой CI-пайплайн запускает `npm install` и использует caret-диапазоны в package.json. Коллега предлагает укрепить шаг установки. Выбери самое сильное изменение.

Викторина

Какое единственное наблюдение привело к обнаружению бэкдора xz-utils (CVE-2024-3094)?

Викторина

Почему атака dependency confusion срабатывает, даже когда у твоего внутреннего пакета валидная версия?

Расставь шаги по порядку

Расставь защиты от самой дешёвой/немедленной к самой организационной:

  1. 1 Закоммитить lockfile и перевести CI на `npm ci` с integrity-хэшами
  2. 2 Запинить точные версии и добавить `--ignore-scripts` к установкам
  3. 3 Включить автоматическое ревью зависимостей (Dependabot / audit) на каждый PR
  4. 4 Scoped-имена + приоритет приватного реестра; резервировать внутренние имена публично
  5. 5 Генерировать SBOM и требовать SLSA provenance + подписанные артефакты для релизов
Вспомните перед уходом
  1. 01
    Коллега говорит: «мы пиним версии в package.json, значит, мы защищены от атак на цепочку поставок». Объясни, от чего это защищает, а от чего — нет.
  2. 02
    Пройди по тому, как работает атака dependency confusion, и по многослойной защите от неё.
Итог

Код, который ты пишешь сам, — тонкий слой на глубоком стеке зависимостей, что ты не писал, поэтому современная поверхность атаки — это шаг установки, а не твоя прикладная логика. Задокументированные инциденты размечают поверхность: xz-utils показал, что доверенный мейнтейнер может отгрузить бэкдор в артефакте сборки, чей исходник выглядит чисто; dependency confusion показал, что публичные реестры могут затенять внутренние имена более высокой версией; typosquatting и компромиссы event-stream / ua-parser-js показали, как похожий или угнанный-но-реальный пакет проскальзывает. Защиты выстраиваются слоями. Базовый слой — закоммиченный lockfile с integrity-хэшами, ставящийся через npm ci (никогда голый npm install), с точными пинами и --ignore-scripts. Выше — автоматическое ревью зависимостей ловит известно-плохие версии, scoped-имена плюс приоритет приватного реестра убивают confusion, а SBOM плюс SLSA provenance и подписанные артефакты отвечают на вопросы, которые хэшам не под силу: что внутри сборки и не подменили ли её после написания исходника. Ничего экзотического — в основном это конфигурация, которую ты включаешь до того, как тебя найдёт следующий сюрприз на 500ms.

Продолжить восхождение ↑Supply-chain безопасность: тест с множественным выбором
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.