HN 日本語サマリー

← 一覧へ戻る
セキュリティ

ChatGPTアカウントを乗っ取る方法

We Took over ChatGPT Accounts (parameter.ai)

101 pointsby armcat44 コメント

要約

2026年10月、研究者たちはChatGPTの共有機能の脆弱性を発見し、悪意のある会話を公開し、そのリンクを他のユーザーに送信することで、受信者の認証済みセッション内で任意のJavaScriptを実行できることを実証しました。この脆弱性を悪用すると、攻撃者は被害者の会話リストを列挙したり、プライベートな会話の内容を取得したり、被害者のアカウントとしてChatGPTのAPIを直接呼び出すことが可能でした。OpenAIはこの問題を迅速に修正し、P1(最重要)と分類しました。

全文翻訳

計算しよう:ChatGPTアカウントを乗っ取った方法

2026年10月、私たちは通常の公開ChatGPT共有を、開いたユーザーのサインイン済みセッション内で実行される攻撃者制御のJavaScriptに変換する方法を発見しました。

攻撃者は悪意のある会話を作成し、ChatGPTの通常の共有機能を使用して公開し、そのリンクを別のユーザーに送信できます。被害者が共有リンクを開くと、ペイロードは被害者の認証済みWebセッションにアクセスできる状態で chatgpt.com として実行されました。

このコードが被害者の会話リストを列挙し、攻撃者と共有されたことのないプライベートな会話の内容を取得し、その会話からのデータを外部サーバーに送信できることを確認しました。ペイロードは、ページの変更やアラートの表示に限定されず、被害者としてChatGPTの認証済みAPIを直接呼び出すことができ、実質的に被害者のChatGPTアカウントを乗っ取ることが可能でした。

被害者はアプリをインストールしたり、プロジェクトに参加したり、コードを貼り付けたり、開発者ツールを開いたりする必要はありませんでした。共有リンクを開くことが唯一必要なアクションでした。

OpenAIは10月26日に問題を修正し、報告をP1と分類しました。

この連鎖は、単独では無害に見える2つの機能、グラフとHTMLプレビューから始まりました。

flowchart TD
  A["攻撃者がChatGPTの会話を公開"] --> B["被害者が公開共有を開く"]
  B --> C["HTMLプレビューがメインのchatgpt.comページに侵入"]
  C --> D["JavaScriptが被害者のサインイン済みセッションで実行される"]
  D --> E["被害者としてのchatgpt.comでのあらゆる操作:
プライベートなチャットやファイルを読み取る、アカウントを変更する"]
技術的な詳細

グラフとプレビューから始まった

この研究を開始した疑問は単純でした。プレビューコードが別のウェブサイトに閉じ込められた場合、それは何に通信できるのか?

ChatGPTは、そのプレビュー機能を使用して、HTMLコードブロックを小さな動作するウェブページに変換できます。そのページには任意のコードが含まれる可能性があるため、ChatGPTはそれを別のウェブアドレス上の制限されたフレーム内で賢く実行します。プレビューはJavaScriptを実行できますが、ブラウザはそれを別のウェブサイトとして扱うため、周囲のサインイン済みChatGPTページを直接検査することはできません。

当初、この分離は意図したとおりに機能しているように見えました。プレビュー内のコードは parent.document、つまりメインのChatGPTページを読むことはできませんでした。それで終わるはずでした。

会話には通常のインタラクティブなグラフも含まれていました。バックグラウンドでは、グラフはMathLiveという数学ライブラリをロードしました。それをロードすることは、グラフを描画する以上のことをしました。それはまた、MathLiveのオンスクリーンキーボードに関連するメッセージのために、メインのChatGPTページにリスナーをインストールしました。

ブラウザは関連APIを postMessage と呼びます。これは、ブラウザが互いのページを読むことを防ぐ場合でも、2つのフレームが情報を交換するための正当な方法です。メールスロットのようなものだと考えてください。制限されたプレビューはメインページに歩いて入ることはできませんが、スロットを通してメッセージをドロップすることはできます。

これは、受信者がメッセージの送信元を確認した場合にのみ安全です。

MathLiveは、私たちが関心のある設定に対してそのチェックを行いませんでした。そのリスナーは、仮想キーボードレイアウトを変更するメッセージを受け入れ、そのレイアウトにはHTMLを含めることが許可されていました。グラフがレシーバーをインストールし、プレビューが私たちに信頼されていない送信者を提供しました。2つの無関係な機能が、偶然にもサンドボックスを横断するパスを作成しました。

動作すべきではなかったメッセージ

プレビューは2つのメッセージを送信しました。最初のメッセージは、仮想キーボードレイアウトを私たちのHTMLに置き換えました。2番目のメッセージは、MathLiveにキーボードを表示するように指示し、新しいレイアウトをレンダリングさせました。

top.postMessage({
  type: "mathlive#virtual-keyboard-message",
  action: "update-setting",
  layouts: [{
    label: "Compatibility",
    layers: [{ id: "compatibility", markup }]
  }]
}, "*");

top.postMessage({
  type: "mathlive#virtual-keyboard-message",
  action: "show",
  animate: false
}, "*");

MathLiveは公開メッセージタイプを認識しましたが、そのトップレベルコントローラーは none のオリジンポリシーを使用していたため、任意のウェブアドレスからのメッセージを受け入れていました。また、update-setting メッセージが事前に承認されたウィンドウから送信されることを要求しませんでした。リスナーに到達でき、メッセージ形式を知っている人なら誰でもレイアウトを置き換えることができました。

キーボードが開いたとき、MathLiveは提供されたレイアウトをHTML文字列に結合し、それを実際のChatGPTページ内の innerHTML に割り当てました。createHTML というヘルパーは、当初値をサニタイズするように見えました。しかし、それはそうしませんでした。それは文字列を信頼できるとマークし、私たちのHTMLをそのまま通過させました。

これはエキサイティングでしたが、まだJavaScriptの実行は得られていませんでした。 innerHTML を介して挿入されたスクリプトタグは自動的に実行されません。

次のステップは、 srcdoc を使用する iframe でした。これは、埋め込みフレームが属性に直接HTMLを格納できるブラウザ機能です。

<iframe srcdoc="..."></iframe>

MathLiveは、 sandbox 属性なしでこのフレームをメインのChatGPTドキュメントに挿入しました。これにより、2つの有用なプロパティが得られました。第一に、srcdoc は新しいドキュメントを作成し、外部スクリプトを実行できました。第二に、メインのChatGPTページがフレームを作成したため、ブラウザはそれに https://chatgpt.com のウェブIDを付与しました。したがって、新しいフレーム内のコードは親ページにアクセスできました。

YouTube JSONPによるCSPのバイパス

ChatGPTのコンテンツセキュリティポリシー(CSP)は再び私たちを止めました。CSPは、ページがどのスクリプトをロードできるかを制御し、そのポリシーはインラインJavaScriptを正しくブロックしました。しかし、YouTubeからのスクリプトは許可されていました。

YouTubeのoEmbedエンドポイントは、JSONPスタイルのコールバックパラメータをサポートしていました。JSONP応答は、通常のJSONではなくJavaScriptです。エンドポイントは無効なコールバックを拒否しようとしましたが、応答を返す前にコールバックを応答に配置しました。

https://www.youtube.com/oembed?callback=globalThis.__marker=31337;void

応答は次のように始まりました。

// API callback
globalThis.__marker=31337;void({
  "error": {
    "code": 400,
    "message": "Invalid JSONP callback name: ..."
  }
});

コールバックを ;void で終わらせることで、YouTubeが追加したオブジェクトは無害になりました。YouTubeはすでにChatGPTのCSPによって許可されていたため、ブラウザはこの応答をスクリプトとしてロードし、新しいフレーム内でコールバックを実行しました。

これで、chatgpt.com として任意のJavaScriptを実行できるようになりました。元のプレビューはまだ別のウェブアドレスで分離されていましたが、メインページ内の信頼されたコンポーネントに新しい、サンドボックス化されていないドキュメントを作成させることで境界を越えました。

ラボのバグを1つの公開リンクに変える

これまでのところ、これは有用なサンドボックスエスケープでしたが、最初の動作バージョンは説得力のある現実世界の攻撃ではありませんでした。それは研究者が制御するChatGPTアプリを送信者として使用しました。被害者は最初にそのアプリを接続または呼び出す必要がありました。

次に、ネイティブHTML添付ファイルを通じてペイロードを配信しようとしました。ChatGPTは添付ファイルを同じサンドボックス化されたプレビュー内で開いたため、悪意のあるMathLiveメッセージを送信できました。

しかし、会話を公開共有としてエクスポートしたとき、添付ファイルは残りましたが、以前のグラフは残っていませんでした。グラフは、MathLiveをロードしてメインページに脆弱なメッセージリスナーをインストールしたため、不可欠でした。それがなければ、添付ファイルはメッセージを送信できましたが、誰もそれを聞いて処理していませんでした。

ブレークスルーは、両方の部分を直接1つの通常の公開会話に配置することでした。

  1. 正規のグラフがメインページでMathLiveを初期化しました。
  2. 後続のアシスタント作成HTMLプレビューが2つのメッセージを送信しました。

結果の /share/<id> リンクは、他のユーザーのために完全なチェーンを保持しました。攻撃者は、共有されたChatGPT会話を送信するのと同じ方法でそれを配信できました。電子メール、チャット、サポートチケット、またはソーシャル投稿を介してです。被害者は何もインストールしたり、攻撃者のワークスペースに参加したり、攻撃者が制御するフローを通じて認証したりする必要はありませんでした。単に攻撃者の共有ChatGPT URLにアクセスするだけでよかったです。

相互作用の2つのバージョンを観察しました。最も安定した再現では、被害者は共有を開き、ChatGPTの通常のプレビューボタンを1回クリックしました。その後、HTMLブロックをプレビュー可能としてマークしたため、ChatGPTはそれを自動的に開きました。そのバージョンでは、リンクを開くことが被害者の唯一のクリックでした。別のカウントで、ハードリフレッシュ後も含めて、自動パスを3回再現しました。アラートは約4.5秒後に表示されました。

グラフとプレビューは独立してロードされたため、ペイロードはMathLiveが準備できる前にメッセージを送信することがありました。最終的なキャリアは、限定された期間、同じ2つのメッセージを再試行することで、そのレースを処理しました。メインページ内のガードは、プライベート会話証明とアウトバウンドリクエストが一度だけ実行されることを保証しました。

[画像:公開共有が実際のChatGPTページを変更した]

公開共有が実際のChatGPTページを変更した

この時点で、公開共有はブラウザから chatgpt.com として識別されるアラートを表示でき、メインページにバナーを追加できました。それは私たちのコードがどこで実行されているかを証明しました。それはまだ、なぜバグが重要だったのかを示していませんでした。

アラートだけでは不十分だった

この時点で、アラートを表示することは、エクスプロイトができることの中で最も興味のないことでした。JavaScriptは chatgpt.com として実行されていました。これは、通常のWebアプリケーションと同じ認証済みコンテキストを使用してChatGPTと対話できることを意味します。

ペイロードは、ページですでに利用可能なセッション情報を読み取り、それを使用してChatGPTの会話APIを呼び出しました。まず、被害者の最近の会話を列挙しました。次に、選択したプライベート会話のメッセージデータを要求できました。

GET /backend-api/conversations?...&limit=20&order=updated&offset=0
GET /backend-api/conversations/<conversation-id>?num_turns=2

これは、画面に表示されているテキストをスクレイピングするのではなく、直接認証されたAPIアクセスでした。ターゲットの会話は悪意のある共有とは無関係であり、攻撃者と共有されたことはありませんでした。

応答が注入されたJavaScriptで利用可能になると、データを外部に送信することは簡単でした。ペイロードは、私たちが制御するサーバーへの通常の画像リクエストを作成しました。

意図的に応答の小さな部分のみを外部に送信しました。注入されたコードはすでに認証済み会話応答を受信しており、その内容を読むことができたため、脆弱性はその値に限定されませんでした。

[画像:プライベート会話データが被害者のセッションから流出できることを制御して証明]

プライベート会話データが被害者のセッションから流出できることを制御して証明

結果は、実用的なクロスユーザーデータ盗難プリミティブでした。悪意のある公開会話は、他のユーザーのサインイン済みChatGPTセッション内で実行され、プライベート会話を列挙し、プライベートメッセージの内容を取得し、選択したデータを攻撃者が制御するサーバーに送信できました。

コードはWebアプリケーションの認証済みアクセスで実行されたため、その範囲は証明で使用された会話エンドポイントに固有のものではありませんでした。ChatGPTの認証済みフロントエンドAPIを通じて公開された他のデータやアクションは、同じセキュリティ境界内にありました。基本的に、被害者のChatGPTアカウントの代わりに、あらゆるアクションを実行できました。

何がうまくいかず、OpenAIは何を修正したか

プレビューサンドボックス自体は機能していました。プレビュー内のコードはChatGPTページを直接読むことはできませんでした。問題は、メインページ内の別のコンポーネントが、その指示を受け入れる用意があったことでした。

3つの設計上の決定が、その間違いを完全なエクスプロイトに変えました。

  1. MathLiveは、どのフレームが送信したかを証明せずに、状態変更メッセージを受け入れました。
  2. メッセージ形式は、生のHTMLをキーボードレイアウトデータとして提供することを許可しました。
  3. そのHTMLは、chatgpt.com のIDを継承したサンドボックス化されていないドキュメントを作成できました。

YouTube応答はCSPの最終的な回避策であり、根本原因ではありませんでした。そのエンドポイントのみをブロックしても、JavaScriptへの1つのパスは削除されますが、信頼されていないプレビューがChatGPTにHTMLを挿入できる状態は残ります。

OpenAIは、JavaScript実行とプライベート会話証明の両方を再現しました。修正はChatGPTのWebフロントエンドの2つのバージョンにロールアウトされました。ロールアウト中、CSPの変更は元のチェーンの一部をブロックしましたが、一方のバージョンはまだ更新が必要でした。10月26日、OpenAIは両方のバージョンが報告で使用されたクロスフレームMathLiveメッセージを拒否し、問題を解決したことを確認しました。

防御的な教訓は非常に単純です。

  • メッセージレシーバーは、送信者のウェブアドレスと、権限が与えられた正確なウィンドウの両方を認証する必要があります。
  • クロスウィンドウ設定には、生のHTMLを含めるべきではありません。
  • 動的に作成されたドキュメントは、信頼されていないフレームによってコンテンツが影響を受ける可能性がある場合、サンドボックス化されたままにする必要があります。
  • CSPはバックアップ制御であるべきであり、攻撃者が制御するHTMLがメインサイトに到達した後の唯一の障壁ではありません。

このバグは興味深かったのは、個々の機能がすべて合理的だったからです。プレビューは分離されていました。グラフは数学キーボードをロードできました。フレームはメッセージを交換できました。CSPは主要なメディアプロバイダーを許可しました。公開会話は共有できました。脆弱性は、これらの機能がすべて1つのページに組み合わされたときにのみ現れました。

タイムライン
日付イベント
2026年10月13日ChatGPTのプロダクションコードに対するMathLiveメッセージからHTMLへのパスを確認
10月14日通常のUIを通じてチェーンを再現し、修正されたアラートを任意のJavaScriptにアップグレード
10月16日別のアカウントとワークスペース間での公開共有配信を、1回のプレビュークリックで確認
10月17日共有ナビゲーションからの自動プレビューを確認し、ハードリフレッシュ後に再現
10月18日OpenAIのバグバウンティプログラムに報告を提出
10月19日管理されたプライベートチャット開示証明を追加
10月21日OpenAIが問題を再現。深刻度は初期のP2トリアージからP1に引き上げられた
10月22日OpenAIが一方のWebフロントエンドを修正したと報告。もう一方はまだ更新中
10月23日CSPの変更がチェーンの一部をブロック。一方のフロントエンド更新はまだ完了していない
10月26日OpenAIが両方のフロントエンドが報告されたメッセージを拒否したことを確認し、問題を解決。合計5,000ドルの賞金を授与
技術ノート

上記のメインストーリーには、完全なエクスプロイトチェーンが含まれています。以下の詳細は、プロダクションコードとペイロードの変換をより詳しく追いたい読者のために含まれています。

関連するデミニファイされた制御フローは、次と同等でした。

window.addEventListener("message", (event) => {
  if (event.data?.type !== "mathlive#virtual-keyboard-message") return;
  if (!validateOrigin(event.origin, this.originValidator)) return;
  this.handleMessage(event.data, event.source);
});

if (message.action === "update-setting" && message.layouts) {
  this.layouts = message.layouts;
}

レイアウト正規化は layouts[].layers[].markup を保持しました。キーボードを表示すると、最終的に次の同等の処理が行われました。

keyboardContainer.innerHTML = MathfieldElement.createHTML(keyboardHTML);

createHTML は、サニタイザーではなく、IDまたはTrusted Typesラッパーとして機能しました。Trusted Typesは、HTMLシンクへの割り当てを制限できますが、IDポリシーは攻撃者が制御する文字列を安全にはしません。

パーサーとCSPの移行。 innerHTML 操作はiframeを作成できましたが、直接挿入されたスクリプトは静止したままでした。サンドボックス化されていない srcdoc は、新しいHTMLドキュメントの解析を作成し、埋め込みChatGPTオリジンを継承しました。そのドキュメントは、インラインコードをブロックしましたが、script-src-elem での www.youtube.com を許可した、キャプチャされた有効なCSPも継承しました。

oEmbed応答は、HTTP 200で Content-Type: text/javascript; charset=UTF-8 および X-Content-Type-Options: nosniff を返しました。クロスオリジンリソースポリシーヘッダーは観察されませんでした。Googleはコールバックの山括弧を書き換えたため、ペイロードはアロー関数ではなく、従来の function 構文を使用しました。プログラムを ;void で終了させることで、エンドポイントによって追加されたエラーオブジェクトが消費されました。

プライベート会話証明のために、関数は toString() でソースに変換され、二重引用符は単一引用符に変換され、空白は折りたたまれました。次に URLSearchParams がそれをコールバックにエンコードし、スクリプトURLは <script src> 内でHTMLエスケープされ、その要素全体がiframeの srcdoc 属性に再度エスケープされました。最終的なコールバックは5,533文字、YouTube URLは7,309文字、スクリプト要素は7,337文字、そして最終的なiframeマークアップは7,390文字でした。

認証済み読み取り。 同じオリジンフレームは、親ドキュメントから #client-bootstrap を解析し、セッションアクセス トークンとアカウントIDを抽出し、両方が存在する場合にトークン内のアカウントIDがセッションと一致することを要求しました。これらはインメモリ ヘッダーとして使用されました。

Authorization: Bearer <victim token>
ChatGPT-Account-ID: <victim account>
Accept: application/json

主要なリクエストは次のとおりです。

GET /backend-api/conversations?exclude_conversation_origin=tpp&expand=false&is_archived=false&is_starred=false&limit=20&order=updated&offset=0
GET /backend-api/conversations/<id>?num_turns=2&include_has_versions=true

詳細リクエストで403、404、または405のみが発生した場合、コードはレガシー /backend-api/conversation/<id>?include_full_conversation=true ルートを試行しました。検証では、応答から選択された小さな値のみが送信されました。外部画像リクエストの前に、トークン、アカウント、ブートストラップ、セッション、およびリクエストヘッダーへの参照がクリアされました。

主要な教訓
  • 隔離は、それを越えるすべての橋の強さと同じくらい強いです。特権的なリスナーが間違った送信者からのメッセージを信頼する場合、サンドボックス化されたフレームでも親を侵害できます。
  • postMessage レシーバーは、状態変更の指示を受け入れる前に、event.origin と event.source の両方を認証する必要があります。
  • HTMLインジェクションは、新しいサンドボックス化されていないドキュメントと許可されたサードパーティスクリプトエンドポイントを介してJavaScript実行に変わる可能性があります。CSPは価値ある防御層ですが、安全でないレンダリングパスを安全にすることはできません。
  • 同じオリジンJavaScriptの影響は、被害者の認証済みコンテキストで測定する必要があります。コードが chatgpt.com として実行されると、単にアラートを表示するだけでなく、通常のフロントエンドが利用できるのと同じプライベートデータとアカウントアクションに到達できました。
  • クロス機能テストが重要です。このチェーンは、公開共有、プレビュー、MathLive、フレーム作成、およびCSPの間の隙間に存在し、単一の機能に単独で表示されるものではありませんでした。

より広範な教訓は、セキュリティ境界は個々のコンポーネント内だけでなく、製品全体で維持される必要があるということです。公開または共有可能な表面はすべて、攻撃者が制御する入力として扱われるべきであり、すべてのクロスフレームメッセージは認証されるべきであり、信頼されていないマークアップはレンダリングパス全体でサンドボックス化されたままにする必要があります。