23 сентября в npm и PyPI появились заражённые релизы пакетов MemTensor. Внутри них скрывался кроссплатформенный Go-червь sckit; команда Aikido обнаружила кампанию и назвала её supplychain.localназвание кампании, данное по имени модуля в бинарнике вредоноса. В npm пострадал плагин @memtensor/memos-cloud-openclaw-plugin версий 0.1.21, 0.1.23 и 0.1.25, в PyPI вредоносный код попал в MemoryOS 2.0.34.
Это не первая волна: источники фиксируют пять волн атак, начиная с сентября 2025 года, и отдельно — кампанию Mini Shai-Hulud, которая распространялась через npm и затронула TanStack, Mistral AI, UiPath, Bitwarden и OpenSearch. Разберём, как устроена цепочка заражения, почему стандартные средства защиты её не остановили и что делать, если машина уже заражена.
Что именно изменилось
Ключевой приём Mini Shai-Hulud — файл .vscode/tasks.json, в котором задача помечена как запускаемая при открытии папки. VS Code — легитимный редактор, где tasks.json определяет задачи сборки, тестов и линтинга, а настройки запуска позволяют задаче выполняться автоматически по триггеру. Для доверенных рабочих пространств это происходит молча — до просмотра кода, до загрузки расширений и до того, как разработчик успеет подумать о том, что открыл. Достаточно положить такой tasks.json в репозиторий и заставить разработчика открыть репозиторий в VS Code, чтобы получить удалённое выполнение кодавыполнение произвольных команд на машине жертвы без её явного согласия на его машине.
Приём не нов: он напрямую заимствован из арсенала PolinRider и TasksJacker, применявшегося Lazarus Group с конца 2025 года. Mini Shai-Hulud добавил к нему две вещи. Первая — в .claude/settings.json настраивается хукобработчик, который система запускает автоматически при наступлении определённого события: он срабатывает, когда Claude Code подключается к рабочей области, и вызывает .claude/execution.js. Если разработчик не открывает репозиторий в VS Code, но открывает его в Claude Code — или запускает оба, как делают многие, — полезная нагрузка всё равно выполняется. Вторая — загрузка рантайма Bun v1.3.13 вместо Node.js для запуска обфусцированного execution.js.
Обе техники используют одну идею: открытие папки как триггер выполнения.
Как распространяется заражение
Цепочка начинается с того, что атакующий форкает целевой репозиторий и открывает PR, запускающий workflow с pull_request_target — триггер, который срабатывает на pull request из форка и выполняется в контексте базового репозитория с доступом к его секретам и кэшу, в отличие от обычного PR из форка, у которого такого доступа нет. Это даёт возможность писать в кэш GitHub Actions. Отравленный PR записывает подделанный артефакт сборки в кэш. Легитимный релизный workflow позже восстанавливает этот кэш и использует подделанный артефакт в своей сборке.
Восстановленный кэш содержит бинарники, которые читают OIDC-токентокен аутентификации GitHub Actions, выдаваемый workflow для доступа к внешним сервисам GitHub Actions из памяти раннера (/proc/<pid>/mem). Токен даёт кратковременные, но реальные права на публикацию. Хук preinstall скачивает и тихо устанавливает Bun, после чего запускается обфусцированный router_init.js (~2.3 МБ), который собирает учётные данные: npm-токены, GitHub PAT, ключи AWS/GCP/Azure, SSH-ключи, .gitconfig, .npmrc.
Собранные данные эксфильтруются по трём параллельным каналам: git-tanstack[.]com (DNS C2), Session network и GitHub API dead dropтайники: вредонос оставляет украденные данные в подконтрольных атакующему репозиториях, откуда их забирает, не поддерживая прямого соединения с жертвой в подконтрольных атакующему репозиториях. Украденный npm-токен используется для публикации заражённых версий до 100 собственных пакетов жертвы — это и обеспечивает самораспространение.
В случае sckit внутри найдены команды для npm publish, npm version patch и загрузки в PyPI через twine, а также шаблон GitHub Actions, способный запускать имплант после каждого push. Это позволяет переносить заражение между npm и PyPI.
Почему это сработало
Стандартные средства защиты не остановили волну, потому что атака происходила до или в обход проверяемых ими артефактов.
- SLSA Build L3 provenanceуровень аттестации происхождения сборки, подтверждающий, что артефакт собран защищённым пайплайном оказался неэффективен: внедрение произошло до генерации provenanceсведений о происхождении артефакта — данных о том, как и из чего он был собран, и аттестация оставалась технически валидной. Валидные аттестации не означают чистый код, когда скомпрометирован сам пайплайн сборки.
- 2FA на аккаунтах мейнтейнеров не помогла: OIDC-токен был извлечён из памяти CI-раннера, а не получен через компрометацию аккаунта.
npm auditне предназначен для обнаружения вредоносного внедрения в легитимные пакеты — он ищет известные CVE.- Флаг
--ignore-scriptsна родительском пакете не блокирует выполнениеprepare-хуков в транзитивных зависимостях из git/file/http. - Доверие к известным пакетам не сработало: TanStack, Mistral AI, UiPath, Bitwarden и OpenSearch были скомпрометированы в волнах 1–5.
Предыстория: пять волн
Червь Shai-Hulud развивался волнами, и в каждой менялся вектор проникновения и новизна техники.
| Волна | Дата | Вектор | Масштаб |
|---|---|---|---|
| Wave 1 | сентябрь 2025 | скомпрометированные учётные данные мейнтейнера | 500+ пакетов |
| Wave 2 | ноябрь 2025 | инъекция в CI/CD-конвейер | 796 пакетов, 1 092 версии |
| Wave 3 | март 2026 | пакеты Aqua Security / Trivy | целевая атака |
| Wave 4 | апрель 2026 | npm-пакеты SAP; Bitwarden CLI | целевая атака |
| Wave 5 (Mini) | май 2026 | отравление кэша GitHub Actions + извлечение OIDC | 172 пакета, 403 версии, 518 млн совокупных загрузок |
Волна 2 добавила загрузчик Bun setup_bun.js вместе с bun_environment.js. Волна 3 представляла собой прямую подмену инструментов безопасности. Волна 4 нацелилась на инструменты управления учётными данными. Новизна волны 5 — действительные аттестации происхождения SLSA Build Level 3 на вредоносных пакетах, что обходит стандартные инструменты проверки.
Волна Mini Shai-Hulud / SAP CAP приписывается TeamPCP — той же группе, что заявила о компрометации @bitwarden/[email protected] с полезной нагрузкой «Shai-Hulud: The Third Coming». Остаётся невыясненным, является ли TeamPCP исходной командой Shai-Hulud, преемником с утёкшим инструментарием или подражателем.
Что затронуто
Инцидент затронул 170 npm-пакетов в 15 областях (scopes) и 2 пакета PyPI. Среди npm-областей: @tanstack (42 пакета, только router-экосистема), @uipath (65 пакетов), @squawk (20), @tallyui (10), @mistralai (3), @opensearch-project (1), а также @beproduct, @draftauth, @draftlab, @supersurkhet, @taskflow-corp, @tolka, @mesadev, @ml-toolkit-ts, @dirigible-ai и ряд пакетов без области. Пакеты PyPI: mistralai==2.4.6 и guardrails-ai==0.10.1.
Подтверждённые скомпрометированные версии: @tanstack/react-router и @tanstack/router версии 1.169.5, intercom-client версии 7.0.4. Для ряда пакетов указан только флаг проверки (scrutiny flag) без конкретной версии: @tanstack/react-query, @mistralai/mistralai, @uipath/apollo-core, @bitwarden/cli, @ctrl/tinycolor, @asyncapi/cli. Для SAP CAP-экосистемы указаны mbt 1.2.48, @cap-js/sqlite 2.2.2, @cap-js/postgres 2.2.2 и @cap-js/db-service 2.10.1.
Инциденту присвоен CVE-2026-45321 с оценкой CVSS 9.6 Critical и идентификатор GitHub Advisory GHSA-g7cv-rxg3-hmpx.
Обфускация и персистентность
Полезная нагрузка проходит три слоя обфускации: сначала obfuscator.io, затем шифр подстановки Fisher-Yates, а поверх — AES-256-GCM. Шифр подстановки Fisher-Yates использует PBKDF2-SHA256 со 200 000 итераций. Слои применяются последовательно, каждый следующий накладывается на результат предыдущего.
Червь использует демон персистентности, который опрашивает GitHub каждые 60 секунд. При обнаружении отзыва токена он запускает rm -rf ~/ (или эквивалент для Windows). Поэтому отзыв токена — самое опасное действие на заражённой машине, и его нужно выполнять только после удаления демона.
Сканер qi-scape проверяет персистентность через macOS LaunchAgent (файл и загруженное состояние launchctl), Linux systemd service, дропы в .claude/ и .vscode/, внедрённые GitHub Actions workflows и /tmp droppers.
Порядок восстановления
Авторы инструментов рекомендуют выполнять восстановление в строго фиксированном порядке, поскольку он отражает реальные операционные ограничения. FractalRecursion/shai-hulud-guard задаёт восемь шагов:
- STOP — не отзывать токены
- ISOLATE — отключить сеть
- IMAGE — снять форензик-снимок
- REMOVE — удалить демон
- ROTATE — отозвать GitHub, npm, AWS/GCP/Azure, SSH
- AUDIT — проверить историю публикаций npm
- REBUILD — переустановить ОС с чистого образа
- REPORT — сообщить в npm [email protected] и CISA
Порядок критичен. STOP идёт первым, потому что отзыв токенов до удаления демона запускает rm -rf ~/. ISOLATE идёт до IMAGE, потому что иначе червь продолжает эксфильтрацию во время снятия образа. IMAGE идёт до REMOVE, потому что форензик-доказательства уничтожаются при удалении артефактов демона.
Инструмент shai_hulud_guard никогда не вызывает gh auth logout, npm token revoke, aws iam delete-access-key, gcloud auth revoke, az ad sp credential reset или аналогичные команды. Даже с --auto ротация учётных данных представлена как ручной чек-лист, который оператор выполняет после подтверждённого удаления демона. Отзыв токенов следует делать с другой машины, войдя в npmjs.com и GitHub.
Intrudify/mini-shai-hulud-scanner при CRITICAL-находках предписывает: отключить машину от сети, снять снимок или образ диска, с другой машины отозвать токены на npmjs.com и GitHub, затем очистить заражённую машину. qi-scape/scan-shai-hulud при CRITICAL-находках перечисляет ротацию всех доступных учётных данных, удаление вредоносных пакетов, удаление персистентности, блокировку C2-доменов, аудит GitHub Actions с 2026-05-11T19:20Z и проверку несанкционированных публикаций npm.
Что предлагают для защиты
Сканер предлагает проверять пакет до установки командой --check (для npm) или --check-pypi (для PyPI). Она возвращает оценку риска 0–100 и завершается кодом выхода 1 при оценке ≥ 40 — именно этот код используют wrapper-скрипты, создаваемые --protect, чтобы блокировать установку.
При умеренном риске рекомендуется безопасный порядок: сначала установить пакет с --ignore-scripts, чтобы не выполнять lifecycle-скрипты, затем вручную просмотреть объявленные скрипты, прочитав поле scripts из package.json, затем найти файлы полезной нагрузки по именам router_init.js, setup_bun.js, bun_environment.js, и только после ручной проверки вернуть postinstall через npm rebuild.
Для проактивной защиты --protect в Phase 1 создаёт всегда безопасные файлы: wrapper-скрипты npm_safe.sh/npm_safe.ps1 и pip_safe.sh/pip_safe.ps1, которые запускают --check перед каждой установкой, а также SHA-pinned CI workflowшаблон конвейера GitHub Actions, в котором действия закреплены по хешу коммита, а не по тегу, — это защищает сам конвейер от подмены версии действия и шаблон pre-commit hookзаготовку git-хука, который запускается перед каждым коммитом и позволяет прогнать проверку до того, как изменения попадут в репозиторий. Phase 2 — это опциональные изменения системы, включаемые интерактивно или флагами: --setup-alias (shell-алиас для npm и pip), --setup-npmrc (save-exact=true в проектном .npmrc), --setup-cron (ежедневное сканирование через cron или Task Scheduler) и --install-hook (установка pre-commit hook в .git/hooks/). Каждое изменение Phase 2 обёрнуто sentinel-комментариипарными комментариями-маркерами, которыми помечены добавленные инструментом блоки, чтобы их можно было отличить от пользовательского содержимого, а --unprotect удаляет только эти блоки, не затрагивая ранее существовавший пользовательский контент.
Для защиты от извлечения OIDC-токена рекомендуется ограничивать OIDC-доверие конкретным workflow на конкретной защищённой ветке, а не целым репозиторием.
Ограничения и открытые вопросы
Авторы сканеров прямо перечисляют ограничения своих инструментов. Каждая волна Shai-Hulud приносила новую обфускацию, и zero-dayранее неизвестный вариант вредоносного кода, для которого ещё нет сигнатур варианты не дают находок по паттернам. Runtime-анализнаблюдение за поведением программы во время её выполнения отсутствует: инструмент не исполняет проверяемый пакет и опирается только на статическое содержимое. Пакеты, скачивающие и исполняющие код во время установки, не ловятся сканом tarball — чистый tarball, вызывающий curl после установки, не останавливается на этапе сканирования. В окне 2–8 часов между компрометацией и раскрытием список известных-плохих пакетов молчит. Валидные SLSA BL3 аттестации не означают чистый код при компрометации пайплайна сборки. Транзитивные зависимости из git/file запускают prepare-хуки безусловно.
Нерешённым остаётся то, что нулевой риск-скор не гарантирует безопасность.