Обновили зависимость — получили бэкдор: как это выглядит на сервере
Плановое npm update или composer update — рутина, которую многие гоняют раз в спринт, не глядя в диф зависимостей. Проблема в том, что за одной строчкой в package-lock может стоять не багфикс, а чужой postinstall-скрипт, который тихо ушёл в сеть за две минуты до того, как вы закрыли терминал. Разберём, как атака на цепочку поставок выглядит именно на сервере — какие следы она оставляет и как их найти, пока не поздно.
Содержание
Как устроена атака на цепочку поставок
Смысл supply chain атаки в том, что вам не нужно ломать ваш сервер напрямую — достаточно скомпрометировать что-то выше по цепочке, чем вы неявно доверяете. Для npm/PyPI/composer-экосистем это обычно один из трёх сценариев:
- Захват аккаунта мейнтейнера. Автора мелкого, но широко используемого пакета фишат или у него утекает токен публикации. В новую версию добавляется вредоносный код, версия публикуется в реестр как обычный патч.
- Тайпсквоттинг и подмена имени. Публикуется пакет с именем, похожим на популярный (
reqeustsвместоrequests,expresвместоexpress), в расчёте на опечатку вpip installили на автодополнение в IDE. - Компрометация транзитивной зависимости. Вы сами не устанавливали вредоносный пакет — его подтянула зависимость вашей зависимости, на три-четыре уровня глубже, куда никто не смотрит.
Ключевая деталь — момент срабатывания. Большинство находок эксплуатируют install-хуки: postinstall в npm, post_install/cmd_class в setup.py у PyPI-пакетов, скрипты в composer.json секции scripts. Хук выполняется автоматически при установке, без запуска вашего приложения — вредоносный код может отработать ещё на этапе npm install, до того как вы вообще запустили тесты. Второй по частоте вариант — код, зашитый в сам пакет и активирующийся при импорте модуля или при первом вызове конкретной функции, иногда с задержкой в несколько дней, чтобы не спалиться сразу после публикации.
Для сервера это значит: подозрительной может быть не только работающая прод-нагрузка, но и сам процесс npm ci / pip install / composer install, и CI-раннер, где эти команды выполняются постоянно и с более широкими правами доступа, чем у прод-контейнера.
Первый сигнал: сеть, которая не должна отвечать
Пакетный менеджер, ставящий зависимости, не должен ничего делать в сети, кроме обращений к самому реестру (registry.npmjs.org, pypi.org, packagist.org и их CDN). Если сразу после install/update сервер стучится куда-то ещё — это первое, на что стоит смотреть.
Практический способ поймать момент — просто посмотреть, что открывает процесс, пока идёт установка:
# в отдельном терминале, пока выполняется npm install / pip install
sudo ss -tnp | grep -E 'npm|node|pip|python|composer|php'
# альтернативно - через lsof по PID процесса установки
sudo lsof -i -a -p $(pgrep -f "npm install")
Если под рукой tcpdump, полезнее смотреть не на IP, а на DNS-запросы — по ним видно, к какому домену вообще идёт попытка обращения, даже если соединение потом рвётся файрволом:
sudo tcpdump -i any -n port 53 -l | grep -v -E 'npmjs|pypi|packagist|githubusercontent'
На что реагировать:
- Обращения к доменам, не относящимся к реестру пакетов или к CDN самого продукта — особенно к сырым IP без обратного DNS, к недавно зарегистрированным доменам, к нетипичным TLD.
- DNS-запросы с необычно длинными или похожими на base64 поддоменами — частый паттерн эксфильтрации данных через DNS, когда переменные окружения или содержимое
~/.aws/credentialsкодируются прямо в имя запроса. - Исходящие соединения на порты, нетипичные для веб-трафика (не 443/80), особенно с самого хоста, а не из контейнера приложения.
- Активность именно во время
install, а не во время работы приложения — это отличает бэкдор в install-хуке от обычной телеметрии SDK, которая обычно стучится при рантайме, а не при установке.
Если у вас уже стоит система логирования соединений или SIEM — это тот случай, когда стоит завести отдельное правило алертинга именно на исходящие соединения сразу после запуска пакетных менеджеров, а не полагаться на то, что кто-то заметит вручную. Отдельная статья разбирает, чем отличить взлом от обычного глюка после обновления — сетевая активность в момент установки один из самых надёжных различителей.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверФайлы не там, где им положено быть
Второй след — файловая система. Легитимный пакет пишет только внутри своей папки в node_modules/<package>, vendor/<vendor>/<package> или в site-packages/<package>. Всё, что появляется за пределами этой директории в момент установки, — повод для вопросов.
Куда чаще всего пишут вредоносные install-хуки:
~/.ssh/authorized_keys— добавление своего публичного ключа для последующего доступа по SSH в обход паролей./etc/cron.d/,crontab -lпользователя, под которым шла установка — закрепление через периодический запуск.~/.bashrc,~/.profile,/etc/profile.d/— запуск payload при каждом новом логине в шелл./tmp,/var/tmp— временное хранение скачанного второго этапа полезной нагрузки перед его выполнением.- Файлы systemd unit в
/etc/systemd/system/или~/.config/systemd/user/— закрепление в виде сервиса, который переживёт перезагрузку.
Проверка после подозрительного обновления:
# что изменилось за последние N минут вне ожидаемых директорий пакетов
find / -xdev -mmin -30 -type f \
-not -path "*/node_modules/*" -not -path "*/vendor/*" -not -path "*/venv/*" \
-not -path "/proc/*" -not -path "/sys/*" 2>/dev/null
# отдельно проверить самые частые точки закрепления
cat ~/.ssh/authorized_keys
crontab -l
ls -la /etc/cron.d/ /etc/systemd/system/ | grep -v '^total'
Тут же полезен auditd, если он у вас уже настроен: он покажет не просто факт изменения файла, а какой именно процесс и от чьего имени его создал, что резко ускоряет разбор инцидента — подробная настройка есть в статье про аудит логов сервера через auditd. Без auditd придётся восстанавливать картину по mtime и логам самого пакетного менеджера, что менее надёжно — временные метки легко подделать процессу с достаточными правами, а порядок событий в логах — нет.
Отдельно стоит сверить содержимое package-lock.json / composer.lock / poetry.lock с тем, что реально стоит в node_modules/vendor — расхождение хэшей говорит либо о повреждённом кеше зависимостей (бывает и без злого умысла — разбор одного такого случая есть в статье про отравленный кеш зависимостей, который три недели никто не замечал), либо о том, что кто-то подменил файл в обход официального реестра.
Поведение приложения меняется, а код — нет
Третий и самый неприятный сигнал — когда git diff вашего репозитория пуст, а приложение ведёт себя иначе. Именно этот разрыв («мы ничего не трогали, но что-то не так») чаще всего списывают на «глюк» и упускают реальный инцидент. Смотреть стоит на:
- Новые исходящие запросы во время обычной работы приложения, которых не было в baseline — не только на этапе установки, но и при каждом запросе пользователя или по таймеру. Сравнить легко, если у вас снят снэпшот нормального сетевого профиля до обновления.
- Рост потребления CPU или памяти без роста нагрузки — майнеры и некоторые виды эксфильтрации данных заметны именно так, особенно на процессах воркеров, которые раньше простаивали.
- Новые дочерние процессы у процесса приложения, которых там быть не должно — например, node-приложение внезапно спавнит
curl,sh -c,python3 -c:
# дерево процессов с указанием, кто кого породил
ps -ef --forest | grep -A5 -B5 node
# то же самое через pstree, если установлен
pstree -p $(pgrep -f "node server.js")
- Изменение времени ответа или профиля ошибок без изменений в вашем коде и без роста нагрузки — некоторые бэкдоры перехватывают часть запросов для инъекции своей логики (например, подмена ответов на страницах оплаты), и это заметно как аномалия в APM-метриках или логах nginx, даже если сам код приложения не менялся.
- Расхождение поведения в проде и на чистом окружении — если то же приложение с теми же зависимостями на свежем сервере ведёт себя иначе, это сильный аргумент за компрометацию, а не за совпадение.
Если у вас уже стоит мониторинг метрик приложения — не сетевого уровня, а именно бизнес-метрик и APM — аномалия часто видна раньше, чем вы полезете руками искать файлы. Но полагаться только на дашборд нельзя: хорошо написанный бэкдор старается не менять заметные метрики и активируется точечно, например только для части трафика или только по определённому заголовку запроса.
Практика защиты: пиннинг, changelog, изоляция окружения сборки
Полностью исключить риск supply chain атаки нельзя — вы физически не можете аудировать каждую транзитивную зависимость каждого пакета. Но снизить вероятность и сократить окно уязвимости — можно, тремя привычками.
Пиннинг версий вместо диапазонов. ^4.2.0 или ~4.2.0 в package.json значит, что npm install может подтянуть любую совместимую по семверу версию — в том числе выпущенную час назад и ещё не проверенную сообществом. Фиксация точной версии плюс использование lock-файла и команды npm ci (а не npm install) в CI и на проде гарантирует, что ставится ровно то, что зафиксировано в коммите, а не то, что оказалось самым свежим в реестре на момент сборки:
# npm — воспроизводимая установка строго по lock-файлу
npm ci
# pip — фиксация точных версий в requirements
pip install -r requirements.txt --require-hashes
# composer — установка строго по composer.lock, без пересчёта
composer install --no-dev --prefer-dist
Флаг --require-hashes у pip особенно полезен: он сверяет хэш скачанного архива, так что подмена содержимого пакета под тем же номером версии тоже будет поймана.
Ревью changelog и дифа перед обновлением, а не после. Речь не про построчное чтение исходников каждой зависимости — это нереалистично, — а про минимальную гигиену: смотреть, что реально изменилось между вашей текущей и новой версией, особенно если версия мажорная или если пакет неожиданно давно не обновлялся и вдруг выпустил релиз. Особое внимание — на новые install-хуки в package.json/setup.py/composer.json, которых раньше не было: появление postinstall-скрипта в патч-версии логирующей библиотеки само по себе повод остановиться и разобраться, прежде чем катить в прод. Автоматизировать рутинную часть (открытие PR с дифом при выходе новой версии) можно через инструменты вроде Renovate — это снимает часть ручной работы, но не отменяет необходимость просматривать сам диф перед мержем.
Изоляция окружения сборки от продакшена. Это, вероятно, самая важная мера с точки зрения ущерба, а не вероятности атаки. Если npm install/pip install выполняется на той же машине или с теми же правами, что и прод-приложение, — вредоносный install-хук получает доступ ко всему, что доступно проду: SSH-ключам, переменным окружения с секретами, сетевому доступу к базе данных. Правильная схема:
- Сборка идёт на отдельном сервере или в отдельном CI-раннере, у которого физически нет доступа к прод-секретам, прод-базе и прод-SSH-ключам.
- Из результата сборки в прод уезжает не исходный код с зависимостями, а артефакт — собранный докер-образ или архив, у которого уже нет пакетного менеджера и install-хуков (они отработали на этапе сборки, а не на проде).
- Внутри build-окружения процесс установки зависимостей запускается без излишних прав — не от root, в контейнере с ограниченным сетевым доступом (allowlist только на реестр пакетов), в идеале — через приватный прокси-реестр, который сам кеширует и может блокировать подозрительные пакеты до вас.
Более широкий разбор того, зачем вообще разделять сервисы контейнерами с точки зрения безопасности, — в статье про изоляцию сервисов через Docker. Идея та же: даже если один слой скомпрометирован, ущерб не должен автоматически распространяться на остальные.
Если бэкдор подтвердился: план действий
Если хотя бы два из перечисленных признаков совпали — не пытайтесь «почистить» сервер вручную, удаляя подозрительные файлы один за другим. Вы почти гарантированно упустите что-то из закрепления, а следующий cron-джоб или systemd-таймер тихо восстановит удалённое.
Порядок действий:
- Изолируйте сервер сетевым уровнем — снимите его с балансировщика, закройте исходящий трафик файрволом, оставив только доступ для вашего собственного расследования. Не выключайте сам сервер сразу, если вам важны логи и состояние процессов для разбора — потушить его вы всегда успеете.
- Считайте все секреты на этом сервере скомпрометированными — SSH-ключи, токены API, пароли от БД, доступные с этой машины. Ротация должна пройти по всем, а не только по тем, для которых нашли прямое доказательство утечки.
- Не доверяйте логам и утилитам на скомпрометированной машине — если атакующий закрепился глубоко,
ps,ls,netstatмогут быть подменены, чтобы скрывать процессы и файлы. Разбор безопаснее вести с отдельной чистой машины, смонтировав диск в режиме только для чтения, либо статически слинкованными утилитами, принесёнными снаружи. - Восстанавливайтесь не поверх зараженного окружения, а из чистого бэкапа или с нуля — переустановка системы и повторный деплой из известного чистого состояния кода надёжнее любой «чистки».
Этот порядок универсален для любого подтверждённого взлома, независимо от того, был ли источником компрометации именно пакет зависимости или что-то другое.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Как быстро проявляется вредоносный код после обновления зависимости?
По-разному: install-хуки срабатывают за секунды, в момент установки. Код, встроенный в саму библиотеку и активирующийся при определённом условии, может ждать дни или недели — поэтому мониторинг сети и файлов должен быть постоянным, а не разовой проверкой сразу после апдейта.
Достаточно ли антивируса или ClamAV на сервере, чтобы это поймать?
Нет. Сигнатурные антивирусы ловят уже известные образцы, а свежий код под конкретную атаку обычно сигнатур не имеет. Полезнее поведенческий мониторинг — сеть, файлы, процессы, — а не сигнатурное сканирование как единственная линия защиты.
Стоит ли полностью отказаться от автообновлений зависимостей из-за этого риска?
Нет, полный отказ опаснее в среднем — вы теряете патчи реальных уязвимостей. Разумный компромисс — автоматизировать создание PR с обновлением и ревью, но не автомерж без просмотра дифа, особенно для мажорных версий.
Помогает ли npm audit или аналоги от такой атаки?
Отчасти и постфактум. Эти инструменты сверяются с базами уже известных вредоносных пакетов, но не защищают в первые часы после публикации нового релиза, пока о нём никто ещё не сообщил.
Можно ли доверять пакету только потому, что у него миллионы загрузок в неделю?
Популярность снижает вероятность, что пакет вредоносен изначально, но не защищает от захвата аккаунта мейнтейнера в будущем — популярные пакеты чаще выбирают целью именно из-за масштаба потенциального поражения.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →