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

プレビューはホームフォルダを読めた

10 月 14 日、Noma Labs が Claude Code Desktop のプレビューペインの欠陥 SneakPeek を公表した。256 KB を超えるまで水増しされ、テストカバレッジレポートに見せかけた HTML ファイルが、スクリプトポリシーなしでディスクから読み込まれ、その JavaScript が開発者のホームフォルダから SSH 鍵、AWS のシークレット、kubeconfig、Claude 自身の OAuth トークンを読み取り、バックグラウンドのビーコンで外へ送っていた。Anthropic は CVSS 8.7 と評価し、8 月に修正した。Bromure Agentic Coding では、ページはあなたのファイルを一切持たない使い捨ての別 VM でレンダリングされ、エージェントのホームには鍵ではなく身代わりが置かれている。

リポジトリは普通に見えた。ファイルはカバレッジレポートだった。開発者はエージェントにそれを見てほしいと頼み、あとはプレビューペインがやった。

開発者がリポジトリをクローンし、中にあるテストカバレッジレポートを見てほしいと Claude Code に頼む。ごく日常的な依頼だ。レポートは HTML ファイルである。Claude Code Desktop では、Claude が HTML ファイルを読んだり編集したりすると、プレビューペインが確認なしで会話の横にそれをレンダリングする。このレポートにはスクリプトが入っている。数秒のうちに、スクリプトは開発者の SSH 秘密鍵、AWS キー、npm と GitHub のトークン、Kubernetes の認証情報、そして Claude 自身の OAuth トークンを読み取り、すべてを攻撃者のサーバーへ送ってしまう。

これが、Noma Labs が 10 月 14 日(水)に SneakPeek という名前で公開した概念実証だ。CSO Online も同日に報じた。Anthropic はこの欠陥を CVSS 8.7(High)と評価し、その影響をホスト上でのコード実行と同等に扱った。Noma が報告したのは 7 月 4 日。最初のパッチが回避されたあと、修正は 8 月 17 日に Claude Desktop 1.30096.1 として自動アップデートで配信された。GitHub のアドバイザリと CVE はまだ発行待ちだ。

バグは修正された。だが、それを探す価値があった構図は、ほとんどの開発者のノート PC に今も残っている。信頼できないコンテンツをレンダリングするものが、あなたとして動き、あなたの持ち物すべての隣にいるという構図だ。

三つの防御とひとつのサイズ制限

プレビューペインは Electron アプリの中の Chromium パネルだ。Noma はその周りに三つの防御を数え、そのそれぞれに穴を見つけた。

小さなファイル向けのスクリプトポリシー。 256 KB 未満のファイルは、Content Security Policy 付きの base64 の data: URL として読み込まれていた。Content Security Policy は、どのスクリプトの実行を許すかをブラウザに伝えるルールの一覧だ。このポリシーはスクリプトを遮断していた。256 KB を超えるファイルは一時ファイルに書き出され、file:// URL としてディスクから読み込まれ、その経路にはポリシーが適用されなかった。レポートを 256 KB 超に水増しするのは簡単だ。本物のカバレッジレポートでもその程度の大きさはよくある。

ディスクを自由に歩けるオリジン。 ページのオリジンが、そのページに何を読めるかを決める。data: URL には何も読めない不透明なオリジンが与えられる。file:// から読み込まれたページには、Noma の言葉を借りれば「被害者のローカルファイルシステム全体への同一オリジンアクセス」が与えられていた。スクリプトは一時ファイルのパスからホームフォルダを割り出し、fetch() と XMLHttpRequest でファイルを読んだ。

オンになっていなかったフィルター。 ペインには file:// へのアクセスを遮断するためのリクエストフィルターがあった。これはプロキシ経由の開発サーバーには効いたが、ディスクからレンダリングされる静的 HTML ではオフのままだった。

データを外に出すため、スクリプトは navigator.sendBeacon() を呼んだ。バックグラウンドで一方通行の HTTP POST を送る Web API だ。ペインがどこにリクエストを送れるかを制限するものは、アプリの中に何もなかった。Noma のレポートは、WebRTC 経由で取得した開発者の本当の IP アドレスも挙げている。

7 月 24 日の Anthropic の最初のパッチは、ページから file:// パスへの fetch() と XMLHttpRequest を遮断した。その日のうちに Noma は読み取りを blob: URL から起動した Web Worker に移した。パッチが対象にしていなかったコンテキストで、三つの欠陥はすべて再び機能した。8 月の最終的な修正は、ポリシーの穴、file:// での読み込み、Worker の経路をまとめて塞いだ。Noma の研究者 Eli Ainhorn は CSO にこうまとめている。「保護は本物だった。ただ、すべての経路を覆ってはいなかった」

ノート PC:レポートを見ただけで送信公開リポジトリsrc/ tests/coverage/index.html256 KB 超に水増し隠しスクリプト「レポートを見て」プレビュー:Mac 上であなたとして動く256 KB 未満:data: URL、遮断256 KB 超:file:// URL、ポリシーなしディスク全体と同一オリジンfetch ~/.ssh/id_ed25519fetch ~/.aws/credentialsfetch ~/.kube/configfetch ~/.docker/config.jsonfetch Claude OAuth確認もクリックもなし攻撃者のサーバーsendBeacon POST-----BEGIN OPENSSHaws_secret_access…client-key-data:…npm/GitHub トークン本当の IP もWebRTC でエージェントはコマンドを実行していない。読んだのはレンダラーで、権限はあなたのアカウントのものだった。
ノート PC 上の SneakPeek。256 KB を超えるまで水増しされたカバレッジレポートがスクリプトポリシーをすり抜け、ホームフォルダ全体にアクセスできる状態でディスクから読み込まれ、開発者の鍵を読み取ってビーコンで外へ送る。開発者がしたのは、ファイルを見てほしいとエージェントに頼んだことだけだ。

エージェントの仕業ではない

SneakPeek はプロンプトインジェクションを使わない。エージェントはあなたが読むよう頼んだファイルを読み、エージェントを包むアプリがそのファイルをレンダリングした。スクリプトはプレビューのレンダリングプロセスの中で、アプリが持つあなたのディスクへのアクセス権で動いた。

Sysdig の Crystal Morin は CSO に対し、エージェントレベルのログではこの実行を捉えられない可能性があり、防御側はランタイム層でファイルの読み取りと外向きの接続を監視する必要があると語った。セッションの記録に残るのは、無害な依頼と無害な返答だけだろう。

慎重なモデルも、厳格な権限ルールも、コマンドを一つずつ承認する人間も、何も変えなかったはずだ。レンダラーは Mac の上で、あなたとして、あなたのホームフォルダとそこに溜まったすべての認証情報の隣で動いていた。

Ainhorn が CSO に語った助言は、その後半を狙ったものだ。長期間有効な認証情報を開発者のマシンに置かず、範囲を絞った短命のトークンを使うこと。

同じレポートを Bromure のマシンで

Bromure Agentic Coding では、各エージェントは Mac 上の専用の Linux VM(マシンと呼ぶ)の中で動き、Web ページをレンダリングする組み込みブラウザは、その隣の二つ目の使い捨て VM で動く。Noma のリポジトリをマシンにクローンし、カバレッジレポートを見てほしいと Claude Code に頼んでみよう。

レポートを読むことはテキストを読むこと

エージェントは coverage/index.html をほかのファイルと同じように、コンテキスト内のテキストとして読む。チャットでは何もレンダリングされない。Files ペインでファイルをクリックすると、詳細ビューは HTML をハイライトされたソースとして表示する。.py や .json と同じ扱いだ。ページのスクリプトは一つも動かない。512 KB を超えるファイルはプレビューすらされない。スクリプトポリシーを避けるために 256 KB 超に水増しされたレポートは、300 KB のマークアップとして見えるだけだ。

レンダリングは別のマシンで

レポートをレンダリングして見たいときは、エージェントがマシンからフォルダを配信し、マシンの IP アドレスでブラウザペインに開く。ブラウザは完全な Chromium で、専用のサイドカー VM で動く。マニュアルの言葉では、「エージェントがホームディレクトリに書いたファイルは、ブラウザのディスクにはない」。

スクリプトは動くが、読むものが何もない。ページのオリジンは http://192.168.64.x:8000 という普通の Web オリジンだ。Chromium はこのオリジンに file:// パスを読ませない。Worker やブラウザのバグでそれを突破したスクリプトがあったとしても、行き着く先はブラウザイメージのコピーオンライトの一時クローンだ。~/.ssh も ~/.aws も kubeconfig も OAuth トークンもなく、あなたのプロジェクトも一つもない。一時パスから割り出したホームフォルダは、空っぽの Chromium VM のものだ。ビーコンは空のまま出ていく。

既定では、ブラウザはプロファイルを保持しない。Stay signed in to websites はオフなので、ブラウザを閉じると Cookie もログインも消え、非表示のブラウザは 5 分後に破棄される。WebRTC は、マシンの Browser 設定でカメラかマイクを許可しない限りオフのままなので、IP アドレスの手口は呼び出す相手がない。

エージェントのホームにあるのは身代わり

今度は同じスクリプトをエージェント自身の VM に向けてみよう。リポジトリのテストコマンドが自分でレポートを書き出して開き、コードがエージェントの作業場所で動くとする。そのホームフォルダで Noma のリストを順にたどる。

  • SSH 秘密鍵。 存在しない。SSH_AUTH_SOCK の先は Mac へのエージェントブリッジだ。VM は ID の一覧を取得し、署名を依頼できるが、秘密鍵のバイト列がホストを出ることはない。ls ~/.ssh/id_* は何も見つけない。
  • AWS。 ~/.aws/config はヘルパーを指しており、そのヘルパーは本物のアクセスキー ID に 40 文字の偽シークレットを添えて返す。Mac が AWS への途中で、各リクエストを本物のシークレットで署名し直す。
  • Kubernetes。 ~/.kube/config は合成されたものだ。kubectl は Mac 上のプロキシと話し、本物のクラスター認証情報はプロキシが持っている。
  • Docker、git、npm のトークン。 ~/.docker/config.json と ~/.git-credentials には、本物と同じ形をした身代わりが入っている。.env から、あるいは手で取り込んだトークンは、それぞれのホストに限定された身代わりになる。
  • Claude の OAuth トークン。 Mac に残る。エージェントはダミーのキーで動き、Anthropic へのリクエストではプロキシが本物のアクセストークンを通信に乗せる。

身代わりは仕掛け線でもある。Mac のプロキシは、マシンから出ていくすべてのリクエストのヘッダーとボディを調べ、発行先ではないホストへ向かう身代わりを探す。brm-docker-… のパスワードや偽の GitHub トークンを攻撃者のサーバーへ運ぶビーコンには HTTP 451 が返る。何も転送されず、VM は一時停止し、Mac には This environment may have been compromised という警告が開く。そこにはトークンのプレビュー、認証情報の名前、それが属するホスト、送られようとしたホストが表示される。検出は Security Timeline に赤い行として残る。マシンの Outbound connections ファイアウォールで Unmatched traffic を Deny にしておけば、プロジェクトが使わないホストへの接続はその前に拒否される。

これが、ランタイム層についての Morin の指摘への答えになる。チェックは Mac の上、通信路の上にあり、エージェントの外、VM の中でレンダリングされるあらゆるものの外にある。

Bromure:レンダラーにファイルなし、エージェントに鍵なしマシン VM(エージェント)index.html はテキストFiles ペイン:ソース表示ホーム~/.ssh/id_* (なし)~/.aws 偽シークレット~/.kube/config 合成docker, git 身代わりClaude キー ダミーcoverage/ を :8000 で配信読める本物の秘密はないブラウザ VM(使い捨て)http://192.168.64.x:8000スクリプト実行file:// は拒否新しいクローン、ファイルなしプロファイルなし、WebRTC オフsendBeacon(…) → 空非表示 5 分で破棄Macプロキシが本物の鍵を正しいホストにだけ身代わりが別ホストへ:451、VM 停止、警告未知のホストは拒否Security Timeline:赤い行に両ホスト両 VM の外、Mac に保管SSH 秘密鍵 · AWS シークレット · クラスター認証情報 · レジストリと git のトークン · Claude OAuth トークン · Security Timeline
Bromure での同じレポート。エージェントはテキストとして読み、Files ペインはソースとして表示する。レンダリングするときは、あなたのファイルを一切持たない使い捨てのブラウザ VM の中で行われる。エージェントのホームにあるのは身代わりで、間違ったホストへ送られた身代わりはマシンを止める。

修正と、次のプレビュー

Anthropic は 8 月に SneakPeek を修正し、Noma が再テストした。報告から修正までは 6 週間かかり、最初のパッチは公開当日に Web Worker で破られた。レンダラーの修正はこうなりがちだ。ブラウザエンジンにはスクリプトを起動する方法もリクエストを送る方法もたくさんあり、防御はそのすべてを覆わなければならない。

コーディングツールは、何かをレンダリングするペインを次々に増やしている。HTML プレビュー、埋め込みコンテンツ付きの Markdown、ノートブックの出力、開発サーバー、エージェントが作ったばかりのアプリのスクリーンショット。どれも、開発者が書いていない、1 分前にクローンしたリポジトリのコンテンツに向けられたレンダラーだ。ノート PC の上では、そのどれもがホームフォルダの隣で動く。

Bromure では、ページをレンダリングするペインはあなたのファイルを一切持たない VM で動き、エージェントが作業するマシンには身代わりしかない。そこでレンダラーのバグが露わにするのは、使い捨ての Chromium と、どのサーバーも受け付けない文字列だけだ。

ページではなくソース

エージェントは HTML をテキストとして読み、Files ペインはハイライトされたソースとして表示する。レビューのためにレポートを開いても、スクリプトは一つも動かない。

ディスクに何もないブラウザ

ページは使い捨ての別の Chromium VM でレンダリングされる。エージェントのファイルも Mac のファイルもそのディスクにはなく、閉じればすべて忘れる。

身代わりだらけのホームフォルダ

SSH 秘密鍵はなく、AWS のシークレットは偽物、kubeconfig は合成、レジストリと git のトークンは身代わり。本物の認証情報は Mac 上で暗号化されたままだ。

通信路の上の警報

発行先でないホストへ送られた身代わりには 451 が返る。VM は一時停止し、警告が両方のホストを示し、Security Timeline に記録が残る。

レポートを見てもらおう

「カバレッジレポートを見て」は、コーディングエージェントへの依頼としてこれ以上ないほどありふれたものだ。開発者はこれからも自分が書いていないリポジトリのファイルについて尋ね続け、ツールは役に立とうとしてそうしたファイルをレンダリングし続ける。

その依頼は Bromure のマシンでしよう。そこではページは読むもののない VM でレンダリングされ、エージェントの鍵は身代わりだ。身代わりと、それが鳴らす警報については、エージェントは自らの鍵を漏らしたと鍵はあなたの .env ファイルだったを読んでほしい。