Блог / Новости безопасности

Взлом axios в npm: как порвалась цепочка доверия

Взлом axios в npm — не уязвимость в коде. Разбираю шесть звеньев цепочки доверия, меры, которые рвут каждое звено, и что изменил npm 12.

Аспирантура по информационной безопасности завершенаВнутривузовская защита пройдена · Итоговая защита диссертации впереди

Код был в порядке. Издатель — нет

Две записи в реестре, одна версия разницы. Axios 1.14.0 опубликовал GitHub Actions через доверенного издателя, и у релиза есть provenance. Axios 1.14.1, вышедший в 00:21 UTC 31 марта 2026 года, выложили вручную, без всего этого. Эта разница и есть вся история взлома axios в npm, и она лежала в метаданных реестра. [2]

Факты коротко. Версии axios 1.14.1 и 0.30.4, опубликованные с разницей в 39 минут, добавили одну новую зависимость, plain-crypto-js@4.2.1, чей скрипт postinstall ставил троян удалённого доступа на любую машину, где выполнялась установка. К 03:15 UTC npm удалил обе версии. [1][2] Google с высокой уверенностью приписывает операцию UNC1069, группе, связанной с КНДР, а Microsoft называет актора Sapphire Sleet. [3][4]

Я называю это атакой на цепочку доверия. Атака на цепочку поставок ПО меняет код, который вы запускаете, не меняя ни строчки из того, что вы ревьюили: она компрометирует канал доставки зависимости. Уязвимость в исходниках axios не использовали. Каждое звено — чьё-то решение доверять, и именно так эту историю полезно читать. Сам я ничего из этого не воспроизводил: разбор построен на отчёте сопровождающих, материалах вендоров и записях реестра по состоянию на 7 октября 2026 года.

axios@1.14.0   published by CI
  _npmUser           GitHub Actions
  trustedPublisher   github
  gitHead            46bee3dea75ef53a8eae49f3b7487e6341de6074
  dist.attestations  SLSA provenance v1

axios@1.14.1   2026-03-31T00:21:58Z, published by hand
  _npmUser           the lead maintainer's account
  trustedPublisher   absent
  gitHead            absent
  GitHub commit/tag  none; the release exists only on npm

Цепочка началась на ноутбуке и закончилась на ноутбуке

Первым звеном были не npm и не GitHub. По отчёту сопровождающих, атакующий добрался до компьютера ведущего сопровождающего через целевую социальную инженерию и RAT-вредонос, а это дало учётные данные npm, которыми и опубликовали релизы. [1] RAT на машине разработчика породил токен. Токен породил RAT на каждой машине разработчика и каждом CI-раннере, где поставили отравленную версию. Один и тот же класс вредоносов на обоих концах.

Сопровождающие высказались прямо: публикация из личной учётной записи была риском, которого можно было избежать, а процесс OIDC и неизменяемые релизы, к которым они переходят сейчас, должны были появиться до инцидента. [1] Эта фраза мне нравится. Она называет настоящий урок, и он не про axios, а про то, где живут права на публикацию.

Я читаю инцидент как шесть звеньев, они на схеме ниже. В каждом звене можно было порвать цепочку, и у каждого свой владелец. Часть звеньев принадлежит издателю. Большая часть — вам. Дальше иду по ним по порядку.

1  maintainer laptop    social engineering + RAT         -> npm credentials
2  registry gate        direct token publish accepted    -> axios 1.14.1, 0.30.4
3  decoy dependency     plain-crypto-js 4.2.0 -> 4.2.1   -> new line in package.json
4  your resolver        fresh install, no cooldown       -> poisoned tarball fetched
5  install script       postinstall: node setup.js       -> dropper runs
6  runner or laptop     reachable secrets, open egress   -> RAT, then whatever it can reach

Почему OIDC trusted publishing не остановил украденный токен?

Потому что это была не единственная дверь. Trusted publishing позволяет реестру принимать публикацию только от указанного CI-процесса, по краткоживущему OIDC-токену вместо хранимого секрета. Релизы axios 1.x уже шли этим путём, поэтому у 1.14.0 есть доверенный издатель и provenance. [2][6] Вредоносную 1.14.1 опубликовали напрямую из учётной записи: без доверенного издателя, без gitHead и без соответствующего коммита или тега в репозитории. [2]

Хороший путь публикации не отменяет плохой. Пока пакет принимает токены, любой, у кого токен есть, обходит workflow целиком. Закрывает это настройка пакета «Require two-factor authentication and disallow tokens»: документация npm рекомендует включать её после настройки trusted publishers, и сами trusted publishers продолжают работать как раньше. [6] StepSecurity читает отсутствующие метаданные как кражу долгоживущего классического токена; сопровождающие говорят лишь, что атакующий получил учётные данные аккаунта. [1][2] В любом случае публикация workflow не касалась.

Моё мнение: включить OIDC и не отключить токены — это полконтроля, и команды ставят галочку после первой половины. Если вы публикуете пакеты, проверьте обе половины сегодня. Если вы их потребляете, метаданные реестра — сигнал, который можно читать: версия, которую раньше публиковал доверенный издатель, а теперь нет, заслуживает паузы. Npm затягивает гайки и с другой стороны: с января 2027 года гранулярные токены, обходящие двухфакторную аутентификацию, потеряют возможность публиковать напрямую. [7]

Зависимость, которую никто не импортирует, — это находка

Атакующий подготовил почву за день. plain-crypto-js@4.2.0 появился в 05:57 UTC 30 марта как чистая приманка: копия настоящего кода crypto-js без хука установки. Его единственная задача — дать пакету историю публикаций, чтобы он не выглядел новым. Вредоносная 4.2.1 вышла в 23:59 UTC, за 22 минуты до axios 1.14.1. [2]

Теперь улика. StepSecurity просмотрел все 86 файлов в axios@1.14.1 и обнаружил, что plain-crypto-js нигде не импортируется и не подключается через require. [2] Коду, который поставлял axios, эта зависимость была не нужна. Из релиза к тому же пропал скрипт prepare с husky, что согласуется с ручной публикацией мимо обычных инструментов выпуска. [2] Проще говоря: патч-релиз добавил зависимость времени выполнения, которую пакет не использует. На такой строке ревьюер останавливается.

Эту проверку я бы автоматизировал первой. При каждом обновлении сравнивайте манифест старой и новой версии. Новая зависимость в патч-релизе уходит человеку. Затем три вопроса: насколько стар пакет, кто его сопровождает и использует ли его родитель хоть где-то? Команды ниже отвечают на все три меньше чем за минуту. Бот обновлений этих вопросов за вас не задаёт.

# what changed in the manifest between two versions
npm diff --diff=<pkg>@<old> --diff=<pkg>@<new> package.json

# does the package actually use the new dependency?
npm pack <pkg>@<new> && tar -xzf <pkg>-<new>.tgz && grep -R "<new-dep>" package/

# how old is the new dependency, and who maintains it?
npm view <new-dep> time maintainers --json

Почему одной команды npm install хватило, чтобы отдать машину?

Потому что npm выполняет код при установке. Lifecycle-скрипт — это команда, которую пакет объявляет в package.json и которую менеджер пакетов запускает автоматически; у plain-crypto-js@4.2.1 был объявлен postinstall: node setup.js. [3] Он срабатывает до того, как приложение что-либо импортирует, поэтому проект, который в рантайме вообще не трогал эту библиотеку, всё равно оказался под ударом. StepSecurity зафиксировал первое обращение к серверу атакующего в течение двух секунд после npm install. [2]

Обфускация в setup.js тоньше, чем кажется. Строки разворачиваются, декодируются из Base64 и XOR-ятся с ключом OrDeR_7077 и константой 333. [5] Деобфускация StepSecurity показывает, что ключ проходит через Number(): буквы превращаются в NaN, XOR с нулём ничего не меняет, и реальный ключ — 0,0,0,0,0,0,7,0,7,7. [2] Это приём против сканеров строк, а не криптография. Вывод: статический анализ скриптов установки — полезная сигнализация, но слабая граница.

Дальше дроппер ветвится по платформе и просит у сервера атакующего вторую стадию. Тело POST сообщает серверу, какую отдать: product0 для macOS, product1 для Windows, product2 для Linux. [2] Куда ложится каждая, показано в таблице. Все три варианта шлют маяк раз в 60 секунд, JSON в Base64, с User-Agent от Internet Explorer 8 на Windows XP. [3] Поскольку RAT подтягивается на лету, в tarball вредоносного кода как такового нет: пакет — загрузчик. Поэтому решает исходящий трафик, а IE8 в User-Agent у сборочного раннера — самое дешёвое оповещение, которое вам доведётся написать. [3]

platform  stage two lands in                   started by
win32     %TEMP%\6202033.ps1                   VBScript; powershell.exe copied to %PROGRAMDATA%\wt.exe
darwin    /Library/Caches/com.apple.act.mond   Mach-O binary, chmod +x, zsh in the background
linux     /tmp/ld.py                           nohup python3

all       POST http://sfrclak[.]com:8000/6202033
          body: packages.npm.org/product0 (macOS), product1 (Windows), product2 (Linux)
          beacon: Base64 JSON every 60 seconds
          User-Agent: mozilla/4.0 (compatible; msie 8.0; windows nt 5.1; trident/4.0)
          win32 persistence: HKCU Run key "MicrosoftUpdate" and %PROGRAMDATA%\system.bat

Почему после атаки папка выглядела чистой?

Потому что дроппер за собой прибрал. После запуска setup.js удалял сам себя и вредоносный package.json, затем переименовывал заранее подготовленный package.md в package.json. [2] Этот подменный манифест объявляет версию 4.2.0 без хука установки. Инженер, открывший потом node_modules/plain-crypto-js, видит безобидную 4.2.0, хотя исполнялась 4.2.1. [2] Приманка, подложенная днём раньше, окупается второй раз.

Поэтому я говорю командам: проверяйте lockfile, а не node_modules. Запись о plain-crypto-js в lockfile, версии 4.2.0 или 4.2.1, означает, что граф установки включал атаку, и по рекомендации Google с этого момента машину считают скомпрометированной. [3] Что сохранить и как доказать восстановление, разобрано в моей прошлой статье об Axios и доказательствах сборки. Здесь остаюсь на механизме.

Что делал RAT? В разборе Google полезная нагрузка названа WAVESHAPER.V2; она умеет разведку системы, листинг каталогов, выполнение произвольных команд и внедрение бинарных файлов. [3] Это доступ, а не кража. Что делает оператор с доступом к ноутбуку разработчика или CI-раннеру — читает токены, ключи и файлы .env, которые там лежат; ставки видны на примере OpenAI: workflow GitHub Actions в процессе подписи macOS-приложений запустил axios 1.14.1, имея рядом сертификат подписи и материалы нотаризации. OpenAI не нашла свидетельств доступа к данным пользователей или изменения ПО и в качестве меры предосторожности заменила сертификат. [12]

Кому на самом деле стоит волноваться?

Меньшей части вашего хозяйства, чем следовало из заголовков, и вполне конкретной. Поставляемое приложение не жертва. Ничто не импортирует plain-crypto-js, поэтому у клиентского бандла, собранного из axios 1.14.1, не было причин содержать нагрузку; жертвой стала машина, выполнившая установку. [2] Окно тоже было коротким: с 00:21 UTC до удаления в 03:15 UTC, меньше трёх часов. [1]

Чтобы вас задело, должны были совпасть три условия. Свежая установка разрешила axios 1.14.1 или 0.30.4 в это окно: установка без подходящего lockfile или бот обновлений, смёрживший PR за часы. Скриптам установки разрешили выполняться — это поведение по умолчанию. И на машине было что-то, до чего стоило дотянуться. Во время инцидента Microsoft советовала закрепить точные версии и приостановить автоматические боты зависимостей. [4] Если вы можете ответить на эти три вопроса по каждому раннеру и ноутбуку, вы знаете свою экспозицию. Если не можете — это и есть главная находка.

Восемь проверок перед следующим npm install

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

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

1  lockfile    grep -nE "plain-crypto-js" package-lock.json     # 4.2.0 or 4.2.1 means act
2  CI install  npm ci, never npm install; no bot merges within hours of a release
3  scripts     npm 12: review allowScripts | older npm: --ignore-scripts, allow by name | pnpm 10+: allowBuilds
4  cooldown    npm config set min-release-age 3 | pnpm: minimumReleaseAge
5  trust       pnpm: trustPolicy: no-downgrade
6  publishing  your packages: trusted publisher on, "disallow tokens" on, no publish token on a laptop
7  egress      build runners: default-deny outbound; alert on node spawning curl, powershell or python3
8  secrets     the install job holds no deploy, signing or cloud credentials

Источники

Нужна такая проверка для вашего продукта?

Аудит безопасности кода
Все статьи

08Контакт

Выпускаете продукт с данными пользователей?

Пришлите короткое описание. Я отвечу в течение трёх дней вопросами или наброском предложения, а стоимость согласуем письменно до начала работ.