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

トークンは同じヒープの中にあった

Accomplish の Oren Yomtov は、OpenAI の Codex サンドボックスから二度脱出した。一方の脱出は最も厳しいモードである read-only から始まった。信頼されていない JavaScript が v8.getHeapSnapshot() を呼び、両者が共有するヒープの中から信頼コンテキストの認可トークンを読み取ったのだ。もう一方は apply_patch に自分自身の書き込み権限をファイルシステムのルートまで広げさせ、そのうえで .zshrc に一行を追記した。OpenAI は両方を八日で修正した。だが、それらを生んだ性質には修正パッチがない。番人と、番人が守るコードとが、ひとつのプロセス内の二つの変数だったのである。Bromure Agentic Coding は、番人をあなたの Mac で、エージェントを VM の中で動かす。

非信頼コンテキストはサンドボックスを破ってなどいない。Node にヒープの スナップショットを求め、サンドボックス自身の認可トークンがただの文字列として そこに転がっているのを見つけ、それを使っただけだ。

誰かから送られてきたリポジトリに Codex を向ける。あなたは慎重なので、 read-only で走らせる。エージェントが見ることはできても触れられないモードだ。 エージェントはコードを読む。そのコードの一部はペイロードであり、数秒後には あなたの Mac の上で、サンドボックスの外で、画面に何の確認も出ないまま、 コマンドが動いている。

これが Heapjack、 Accomplish の Oren Yomtov が今月公開した 二つの Codex サンドボックス脱出のうちの一つだ。 BleepingComputer は 9 月 20 日に報じている。 Yomtov は 8 月 12 日に両方を OpenAI に報告し、OpenAI は八日以内に修正した。 Heapjack は Codex Desktop ビルド 26.818.21641 で、もう一方は Codex CLI 0.149.0 で。OpenAI の声明は短いほうの部類だ。「研究者の皆様がご連絡くださり、 発見を共有してくださったことに感謝します。両方の問題には 8 月に対処しましたが、 私たちはサンドボックスを継続的に強化し続けています」

最初のバグがなぜ存在したのか。そこがパッチより長く残る部分である。

ひとつのプロセス、ひとつのヒープ、ひとつのトークン

Codex Desktop には JavaScript を実行する node_repl というツールがある。その 内部に二つのコンテキストを抱えている。親プロセスに実際の処理を依頼することを 許された信頼コンテキストと、エージェントが評価しようとしているものを何であれ 実行する非信頼コンテキストだ。この二つの隔たりが、サンドボックスである。

両方のコンテキストは同じ Node プロセスの中に生きている。Yomtov の言葉で言えば、 「ひとつの Node プロセス、ひとつの V8 アイソレート、両方のコンテキストが共有する ひとつのヒープ。トークンはそのヒープの中の文字列だ」

トークンは、親プロセスが二つを見分けるための手立てである。正しいトークンを 添えてパイプに届いた要求は信頼され、実行される。添えていない要求はされない。 中身はランダムな UUID で、隠す相手のコードがあなたのメモリを読めないかぎり、 秘密としては申し分ない。

読めるのだ。v8.getHeapSnapshot() は Node の公開 API であり、プロファイリング用の 道具であり、ヒープ上のあらゆるオブジェクトを、文字列も含めて、そっくり吐き出す。 非信頼コンテキストはこれを呼び、UUID の形をしたものだけを絞り込み、 ひと握りの候補を手にする。本物を選り分けるにはもう一手いる。ひとつずつ親に 試し、返ってくるものを見るのだ。誤ったトークンには「権限がありません」と返る。 正しいトークンには、要求の残りの部分についての苦情が返る。Yomtov は、 二つめの返答を生んだ文字列を手元に残す。

トークンを手にした非信頼コンテキストは、信頼側が使っているのと同じ stdout パイプに要求を書き込む。親はトークンを照合し、有効だと判断し、サンドボックスの 外でコマンドを実行する。そこから先は、open 経由のシステムコマンド、Docker ソケット、設定ファイルの書き換えである。

これらすべてが read-only からだ。もう一度 Yomtov を引く。「そのすべてが read-only で動いた。最も厳しいサンドボックスモード、エージェントは何も 書けないはずの場所で」

境界と、それが囲むものが、ひとつのアドレス空間にひとつの Node プロセス、ひとつの V8 ヒープ信頼コンテキスト親に実行を頼めるトークンを持つ非信頼コンテキストリポジトリのコードを動かすこれがサンドボックス両者が共有するヒープ「a1f3…-…-…」認可トークン、ただの文字列v8.getHeapSnapshot()非信頼側がすること1. ヒープを取得し UUID 形式を残す2. 「権限なし」か検証エラーか3. 共有 stdout パイプに偽装要求親プロセストークン有効 → サンドボックス外で実行Mac 上でコマンドread-only から、確認なしで
Heapjack はサンドボックスを破らない。サンドボックスの鍵を読むのだ。信頼コンテキストも、非信頼コンテキストも、その二つを区別する認可トークンも、すべてがひとつの Node プロセスの中の、ひとつの V8 ヒープに生きている。v8.getHeapSnapshot() は公開のプロファイリング API で、そのヒープ上のあらゆる文字列を返す。非信頼コンテキストは UUID の形をした候補を集め、'権限がありません' という返答と検証エラーとの違いから本物のトークンを見分け、そのうえで共有された stdout パイプに信頼された要求を偽装して書き込む。親プロセスはトークンを検証し、サンドボックスの外で、何も書き込めないはずの read-only モードで、そのコマンドを実行する。

育ってしまう書き込み権限

二つめの脱出である Overpatch のほうは、もっと素朴だ。Codex CLI にはファイルを 編集する apply_patch というツールがあり、渡されたパッチに書かれたパスから、 どれだけの書き込み権限が要るのかを割り出す。そのパスはモデルから来る。そして モデルは、いまリポジトリを読んでいる最中である。

そこで、パッチのある項目が /tmp を指定する。中身は何も起こさない変更だ。変更は 失敗する。だが、それが暗に求めた権限の付与のほうは失敗しない。書き込み権限は ファイルシステムのルートまで広がる。同じパッチの二つめの項目が、そこでシンボリック リンク越しに $HOME へ書き込み、.zshrc に一行を追記する。

まだ何も脱出してはいない。脱出が起きるのはもっとあと、あなたが端末を開き、 シェルがその一行をあなたとして実行する瞬間だ。その経路のどこにも サンドボックスはない。こちらは日常のモードである workspace-write で動いた。

異なる二つのバグ、異なる二つの製品。そして、その両方を覆う一文が記事にある。 「強制する側のものが、強制される側のものの内側に座っていた」

番人と守られる側が、ひとつのアドレス空間に

抑え込む対象のコードと同じプロセスの中に生きているサンドボックスは、 ひとつの取り決めにすぎない。誰かが本気で覗き込むまでは持ちこたえる。トークンは ヒープの中にある。権限の計算は攻撃者由来のパスを読む。パイプは同じパイプだ。 どれもが直せるものであり、OpenAI はどれも八日で直した。この種の次のバグは、 同じヒープの中の別の文字列だろう。

Yomtov 自身の結論はアーキテクチャの話である。「だからこそ Accomplish は エージェント全体を VM の中で動かしている……ゲストの中にあるものはすべて、 root も含めて信頼しない。何が許されるかを決めるものは、何ひとつその中で 一緒に動いていないからだ」

それが Bromure Agentic Coding の出荷している設計だ。エージェントは、macOS 自身が備えるハイパーバイザーである Apple の Virtualization.framework のもと、 Ubuntu 24.04 のゲストの中で動く。ゲストは構造上、root も含めて信頼されない。 何が許されるかを決めるコードは、vsock の向こう側でホストのプロセスとして動く。 ゲストの内側にあるどのトークンも何ひとつ認可しない。認可するに値するものは、 すべてゲストの外にあるからだ。

プロセス内のサンドボックスひとつの OS、ひとつのユーザーアカウントエージェント他人のリポジトリを読むポリシー同じヒープ内のトークン~/.ssh ~/.aws ~/.zshrc キーチェーン 全リポジトリメモリから読んだ文字列ひとつそれで分離は終わり、上のすべてが手の届く範囲にハイパーバイザーでの境界Linux ゲスト、非信頼、root も含めてエージェント、リポジトリ、自分の $HOMEghp_… sk-ant-… のダミーVirtualization.framework + vsockMac、ここで判断が下されるプロキシ本物の鍵、送信規則、出口での 451ダイアログ「… への書き込みを許可?」VM の外で描画
肝心なのは、どちらのマシンが規則を評価するのかである。左は、番人も、守られるコードも、認可トークンも、あなたの SSH 鍵もクラウド資格情報も .zshrc も、すべてがひとつの OS の中に座っている。だから文字列をひとつ読むだけで隔たりは終わる。右は、エージェントに Linux の VM が渡される。その資格情報は構造だけを保ったダミーであり、ポート 80 と 443 の通信は仮想スイッチがホスト側のプロキシへ迂回させ、ゲストはそれを解除できず、最悪の結末は消してしまえるディスク一つである。root も含めたゲストの完全な侵害が攻撃者にもたらすのは、そのゲストだけだ。

それぞれの脱出が代わりに出会うもの

Overpatch のほうが具体的なので、そこから始めよう。この手口は $HOME の .zshrc に追記して終わる。Bromure のワークスペースでは、ゲストの $HOME は home.img、その VM に接続された ext4 イメージである。あなたの Mac のホーム ディレクトリはその中にない。ゲストがあなたの Mac 上で届く先は、あなたが共有した プロジェクトフォルダだけだ。最大八つ、あなたが選び、ワークスペースエディタに 並ぶもの。あなたが端末を開いたときにシェルが読むドットファイルは、自分でそこに 置かないかぎり、その中には入っていない。

Heapjack のほうが難しい。Bromure の中に、ゲスト内部のヒープスナップショットの 手口を見抜くものは何もないし、見抜くつもりもない。変わるのは、脱出の到着先だ。 Codex では、脱出したコードはあなたの SSH 鍵とクラウド資格情報を握っている アカウントに着地した。Bromure のワークスペースでは、次のような Linux VM に 着地する。

資格情報はダミーである

sk-ant-api03-brm-…、ghp_ の形をした文字列、シークレットが無効な ~/.aws プロファイル。構造は保たれているのでツールは受け付けるが、上流では何の価値も ない。本物の値はホスト側のプロキシのメモリに置かれ、外へ出る途中で差し替え られる。差し替え先は、その値が属するホストか、そのサブドメインに限られる。

持ち出そうとすれば鳴子に触れる

プロキシは、外向きのすべてのリクエストのヘッダーとボディを、そのワークスペース 自身のダミー目当てに走査する。鋳造された先とは別のところへ向かうダミーは HTTP 451 で遮られ、一バイトも転送されない。VM は一時停止し、赤い 資格情報の仲介 の行がセキュリティタイムラインに現れる。

ネットワークはあなたが書いた一覧である

送信は default allow か default deny で終わる順序つきのルールセットだ。 仮想スイッチは VM のポート 80 と 443 の通信をホストのプロキシへ迂回させる。 環境変数は一切介在せず、ゲストが解除できるものも何もない。編集は動作中の セッションに即座に効く。

push は Mac の上で尋ねられる問いである

Git フォージの資格情報は書き込みポリシーを帯びており、新しいワークスペースの 既定は 書き込み前に確認 だ。git push は書き込みである。ダイアログは その操作そのものを名指しし、あなたのデスクトップに描画される。VM の中の 攻撃者は、それをクリックできない。

それでも事が起きたときは、メニュー項目ひとつで復旧する。侵害時のフローは ワークスペースのディスクとホームを消し、あなたの設定とトークンと SSH 鍵は残す。 本物の資格情報はあなたの Mac から一度も出ていないので、ローテーションするものは 何もない。ディスクをリセット はシステムイメージを再クローンする。Bromure の どの操作も、共有されたプロジェクトフォルダには触れない。確認画面にもそう書いてある。

最も厳しいモードが、負けたほうだった

この話に出てくる開発者は、何ひとつ間違っていない。リポジトリが自分のものでは なかったから、わざわざ、そのツールが用意する最も厳しい設定を選んだのだ。 read-only は本物の機能であり、本物の仕事をしていた。共有されたヒープの中の 文字列ひとつが、それをただの提案に変えてしまうまでは。

モードも、許可リストも、承認のプロンプトも、持っている価値はある。そしてその どれもが、それを強制しているコードが、制約する相手のコードの手の届かない場所に 座っていることを前提にしている。それはメモリ配置についての主張であり、 確かめに行けるものだ。Yomtov は Codex を二度、二つの製品にまたがって確かめ、 どちらも答えは「いいえ」だった。

だから、その答えがもはや問題でなくなる場所でエージェントを動かせばいい。root を与え、好きなものを入れさせ、誰かから送られてきたリポジトリを読ませ、そして あらゆる資格情報、あらゆる宛先の判断、あらゆる push の確認を、ハイパーバイザーの 向こう側に置いておく。次の脱出が到着するのは、あなたがもとより捨てる気で いたマシンの中になる。