技術 約6分で読めます

CloudflareのBot管理スクリプトがReactの内部状態まで読んでいた

ChatGPTを開くと、テキストボックスに文字を打てるようになるまでわずかな「間」がある。あの瞬間に何が起きているのか、暗号化されたバイトコードを復号して丸裸にした解析記事が公開された。

Buchodi氏が公開したリバースエンジニアリング記事によると、ChatGPTのログインフローではCloudflareのBot管理スクリプトが動作しており、ブラウザのハードウェア情報だけでなく、ReactアプリケーションのFiber内部状態まで検査している。

Turnstileの裏側にあるカスタムVM

Cloudflare Turnstileは、従来のCAPTCHAを置き換える非対話型のBot検証システムだ。ユーザーにパズルを解かせる代わりに、バックグラウンドでJavaScriptチャレンジを実行してブラウザの真正性を判定する。モードは3つあり、Non-interactive(完全自動)、Managed(疑わしい場合のみチェックボックス表示)、Invisible(UIなし)から選べる。

ChatGPTで使われているTurnstileの内部では、28種類のオペコードを持つカスタム仮想マシン(VM)がバイトコードを実行している。ADDXORCALLBTOARESOLVEBIND_METHODJSON_STRINGIFYといった命令セットで構成され、レジスタアドレスはリクエストごとにランダムな浮動小数点数で割り当てられる。

二重XOR暗号の復号プロセス

Buchodi氏は377個の暗号化プログラムをネットワークトラフィックから収集し、復号手順を確立した。暗号化は二重構造になっている。

graph TD
    A["prepareリクエスト"] --> B["pトークン取得"]
    C["prepareレスポンス"] --> D["turnstile.dx取得<br/>約28,000文字のBase64"]
    B --> E["外層復号<br/>XOR base64decode dx, p"]
    D --> E
    E --> F["外層バイトコード<br/>89個のVM命令"]
    F --> G["19KBブロブ後の<br/>5引数命令を探す"]
    G --> H["第5引数から<br/>浮動小数点キー抽出<br/>例: 97.35"]
    H --> I["内層復号<br/>XOR 内部ブロブ, floatキー"]
    I --> J["本体バイトコード<br/>417〜580個の命令"]

ポイントは、内層のXORキーがバイトコード自体に浮動小数点リテラルとして埋め込まれていること。50回連続で復号に成功しており、再現性は確認済みだ。著者はSDK本体(sdk.js、1,411行)を手動で整形・脱難読化し、28個のオペコードすべてをマッピングしている。

暗号強度としては実質ゼロに近い。XORの鍵が同じデータストリーム内にあるため、カジュアルな閲覧は防げても解析は防げない。Cloudflareとしてはフィンガープリント項目の静的解析を困難にし、何を検査しているかを隠すのが主目的だ。

55項目のフィンガープリント収集

復号されたプログラムは55個のプロパティを3層に分けて収集する。

Layer 1: ブラウザフィンガープリント(38項目)

カテゴリ項目数収集内容
WebGL8UNMASKED_VENDOR_WEBGLUNMASKED_RENDERER_WEBGLWEBGL_debug_renderer_infogetExtensiongetParametergetContextcanvaswebgl
Screen8colorDepthpixelDepthwidthheightavailWidthavailHeightavailLeftavailTop
Hardware5hardwareConcurrencydeviceMemorymaxTouchPointsplatformvendor
Font計測4非表示divを生成→フォント適用→getBoundingClientRectで計測→要素削除
DOM探査8createElementappendChildremoveChilddivstylepositionvisibilityariaHidden
Storage5localStorageにキー6f376b6560133c2cで書き込み、quota.estimateusageを確認

Layer 2: Cloudflareエッジヘッダ(5項目)

サーバーサイドでCloudflareインフラが注入する情報。cfIpCitycfIpLatitudecfIpLongitudecfConnectingIpuserRegionの5つ。クライアント側では取得不可能な情報で、ネットワーク層とアプリケーション層の整合性チェックに使っているのだろう。

Layer 3: Reactアプリケーション状態(3項目)

スクリプトはDOMから以下の3つのReact内部プロパティを読み取る。

  • __reactRouterContext(React Router v6+の内部DOM構造)
  • loaderData(ルートローダーの結果データ)
  • clientBootstrap(ChatGPTのSSRハイドレーション機構)

HTMLを読み込んだだけでJavaScriptバンドルを実行していないヘッドレスブラウザには、これらのプロパティが存在しない。つまりTurnstileは「本物のブラウザか」だけでなく「特定のReactアプリケーションが完全にブートしたか」まで検証している。

行動検知とProof of Work

フィンガープリント収集とは別に、271個の命令からなるSignal Orchestratorが行動分析を実行する。

インストールされるイベントリスナーはkeydownpointermoveclickscrollpastewheelの6種類。これらがwindow.__oai_so_*プレフィックスの36個のプロパティにキーストロークのタイミング、マウスの速度、スクロールパターン、アイドル時間、ペーストイベントの挙動を記録していく。

さらにProof of Work(PoW)チャレンジも組み込まれている。25フィールドのフィンガープリントにSHA-256ハッシュキャッシュを適用し、難易度は40万〜50万イテレーションの一様乱数で設定される。テスト結果では72%が5ミリ秒以内に完了しており、ユーザー体験への影響は最小限に抑えられている。

PoWレスポンスには7つのバイナリ検出フラグ(aicreatePRNGcachesolanadumpInstallTriggerdata)が含まれるが、100サンプルのテストですべてゼロだった。solanaフラグはクリプトマイナー系ブラウザ拡張の検出を狙ったものだ。InstallTriggerはFirefox固有のプロパティで、ブラウザ種別の検証に使われている可能性がある。

トークン生成の最終段階

すべての検証が完了すると、最終4命令が実行される。

graph LR
    A["JSON.stringify<br/>フィンガープリント"] --> B["store"]
    B --> C["XOR<br/>JSON文字列, キー"]
    C --> D["RESOLVE<br/>親フレームへ返却"]

この出力がOpenAI-Sentinel-Turnstile-Tokenヘッダとして、ChatGPTへの会話リクエストに付与される。つまりフィンガープリントそのものが暗号化された形でトークンに埋め込まれ、Cloudflare側で復号・検証される設計だ。

Web開発者にとっての意味

Bot対策の検証レイヤーが、ネットワーク層やブラウザ環境を超えてアプリケーション層にまで到達した。以前はUser-Agent、IPレピュテーション、JavaScriptの実行可否といった粒度で判定していたものが、Reactの内部状態をチェックするところまで来ている。CloudflareはAI向けWAFセキュリティも最近GAにしており、Bot管理の検査対象は今後も広がっていく。

一方で、今回の解析が示したように暗号化の実効性は限定的だ。XORキーが同一ストリームに含まれる以上、決意のある解析者は中身を読める。Cloudflareの設計意図はおそらく「何を検査しているかをサーバーサイドで自由に変更できる柔軟性」にあり、暗号としての堅牢性は二の次だ。検査項目をバイトコードに動的に組み込めるため、新しいBot手法が出てきたときにクライアントSDKのアップデートなしで対応できる。

プライバシーの観点では、localStorage への永続的な書き込み(キー6f376b6560133c2c)とGPU/フォント/画面情報の組み合わせは、かなり精度の高いデバイスフィンガープリントになる。Turnstileのプライバシーポリシー上はBot判定目的に限定されているが、収集される情報量は通常のCAPTCHA代替と比べて多い。