
2026/10/12 16:28
Google で発見した 9 万ドル相当のリモートコード実行脆弱性
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
元のサマリーは、提供された点に基づいて、よく書かれており、正確かつ包括的である。ただし、より精度を高めるために明瞭化できる小幅な改善点として、「第 1 の項目に言及されているがサマリーからは省略されている影響を受けたドメイン」を明示的に列挙することが挙げられる。
以下は、当該特定のドメイン名を取り入れて完全性を最大限に引き出すように微調整されたバージョンである:
改善されたサマリー
Google の共有開発プラットフォーム(developers.google.com、firebase.google.com、および各種 Android サイトを含む)において、セッションクッキーを改ざんするだけで攻撃者が任意のコードを実行できるという重大なセキュリティ欠陥が发现された。核心的な問題は、機密性の高い OAuth クライアントシークレットが適切な保護ではなく、シリアライズされたクッキーペイロード内に不安全に格納されているというアーキテクチャ上のエラーであった;具体的には、Flask のセッション署名鍵がこれらの平文のシークレットを直接元に派生させられていた。この脆弱性により、悪意のある行為者はユーザーのセッションをハイジャックし、Google サーバー上でコマンドを実行するのを容易にできた。研究者らは、改ざんされたクッキー内に悪意のある pickle コードを注入して測定可能なネットワーク遅延を引き起こし、適切でないセキュリティ署名のないリクエストが正しく拒否されることを確認することで、このエクスプロイトの有効性を証明した。リモート特性のために S0 度の严重度として分類され、同年の 8 月に報告された後、2026 年 10 月 7 日に成功裏にパッチが適用された。卓越した発見品質への評価として、Google は研究者らに対し、基本レートに適用された卓越した乗数を基にした賞金 90,000 ドルを授与した。将来のインシデントを防ぐため、Google は機密の暴露を防止するための厳格なセキュリティ対策を実施しており、それは露出したシークレットのローテーション、署名のために高エントロピーを持つサーバーサイドキーの使用、ならびに脆弱なシリアライズデータを安全な不透明参照に置換することで開発者エコシステムを保護することを含んでいる。
本文
Google 開発者サイトにおける OAuth クライアントシークレット漏洩と遠隔コード実行脆弱性の解析
発見までの経緯
Google の開発者ドキュメントサイト(
developers.google.com)において、共有されている OAuth フローを分析した結果、以下の挙動を確認しました。
へのリクエストを送信すると、Flask セッション用クッキーが設定され、その後 Google のサインインページへリダイレクトされます。/oauth2authorize- アカウント作成は不要です。
クッキーの構造分析
通常の Flask セッションは
<ペイロード>.<タイムスタンプ>.<シグネチャ> の 3 つの部分構成ですが、今回のクッキーは約 1,200 バイト という非常に大きな容量を持っていました。これは CSRF トークンとリターン URL を格納するだけの容量としては過大でした。
Flask のセッションクッキーは暗号化ではなく**署名(署名ベース)**されているため、この形式のクッキーを保持していれば誰でも中身を閲覧可能です。先頭のドット(
.)は、ペイロードが zlib で圧縮されていることを示しています。
import base64 import json import pickletools import zlib payload, timestamp, signature = cookie.rsplit(".", 2) def b64decode(value): return base64.urlsafe_b64decode(value + "=" * (-len(value) % 4)) compressed = payload.startswith(".") raw = b64decode(payload[1:] if compressed else payload) session = json.loads(zlib.decompress(raw) if compressed else raw)
クッキー内部の内容
上記スクリプトで得られた JSON 形式の内容は以下の通りです。
{ "_permanent": true, "google_oauth2_csrf_token": "...", "google_oauth2_flow_<csrf>": { " b": "..." } }
- キー
は、Flask がバイト列値であることを示すタグです。" b" - この
の値を Base64 で復号すると、Pickle 形式のデータが得られます。" b"
を実行すると、pickletools.dis
オブジェクトが含まれていることが分かりました。oauth2client.client.OAuth2WebServerFlow
このオブジェクトには以下の重要な情報が含まれていました:
=client_id"<redacted>.apps.googleusercontent.com"
=client_secret
(投稿時点では実際に有効なままでした)b"<redacted>"
脆弱性の本質:署名鍵の漏洩
このクッキーには Pickle シリアライズされたペイロードが含まれていましたが、それ以上に重大なのは以下の点です。
- Google の OAuth
とclient_id
が平文(暗号化されていないまま)で含まれていたこと。client_secret - この取得したクライアントシークレットを Flask の
として試し、発行されたクッキーの署名が検証できるか確認しました。SECRET_KEY - 結果:署名は検証できました。
- つまり、署名鍵は対象となるオブジェクト内部に格納されていたことになります。
一つのシークレット、二つの役割
セッションクッキーの中にクライアントシークレットが漏洩していることは重大な発見ですが、問題の本質は以下の点にあります。
- 同じ値が署名を検証するために使われている。
- これは偶然の幸運ではなく、Flask の
ライブラリが鍵を導出する仕組みに基づいた脆弱性です。itsdangerous
オフラインで署名を再計算し検証した結果、以下の手順で完全な一致を確認しました。
import hmac derived_key = hmac.digest(client_secret, b"cookie-session", "sha1") expected = hmac.digest( derived_key, f"{payload}.{timestamp}".encode(), "sha1", )
- 計算した
の値は、取得したクッキーの署名とバイト単位で完全に一致しました。expected - 偶然ではないことを確認するため、署名の一部(一文字)を反転させると検証が拒否されました。
- 回復されたシークレットを使って元のペイロードを再署名すると許可されました。
これにより、セッションの整合性は完全に失われており、サーバー側が受け入れるような署名を生成するには、セッション内のすべてのキーと値を変更して構わないことが分かりました。
偽造クッキーからコード実行へ
コールバック処理では、セッションからフロー情報を再度読み出して
pickle.loads で復元しています。
セッションクッキー → Flask の署名検証 → google_oauth2_flow_<csrf> の読み取り → バイトデータの Base64 デコード → pickle.loads(...)
- 原則として
は安全なのは、「他者が有効なクッキーを偽造できない」という前提がある場合のみです。pickle.loads - 鍵が公開されると、その前提は完全に無効になります。
任意のペイロードの注入
私は自分のセッションを構築し、任意の CSRF トークンと、任意の Pickle データを対応するフローキーに格納しました。
session = { "_permanent": True, "google_oauth2_csrf_token": "attacker-chosen", "google_oauth2_flow_attacker-chosen": { " b": base64.b64encode(attacker_blob).decode() }, }
パラメータも攻撃者によって決定されるため、サーバー側で何らかの一致チェックを行う必要はありません。state- サーバーは署名を検証し、フローエントリを見つけ出し、バイトデータを
に渡します。pickle.loads
遠隔コード実行(RCE)の実現
Pickle の
REDUCE オペコードは呼び出し可能オブジェクト(関数またはクラス)とその引数を指定しており、復元処理の際に呼び出しが可能となります。公開された署名鍵がこの「危険な処理先」と組み合わさると、**認証されていない遠隔コード実行(Unauthenticated RCE)**が実現できてしまいます。
実際に Google の本番環境でコマンドを実行するつもりはありませんでしたが、
time.sleep(N) を呼び出せることを以下のペイロードで示しました。
import time class Sleep: def __init__(self, seconds): self.seconds = seconds def __reduce__(self): return (time.sleep, (self.seconds,))
| 引数(秒) | 実際の応答時間 |
|---|---|
| 約 0.207 秒 |
| 約 3.193 秒 |
| 約 5.194 秒 |
| 約 8.283 秒 |
応答時間が指定した数値に比例するのは、サーバー側で
モジュールをインポートし実際に time
sleep(N) を呼び出したことを示しています。
コントロール実験
スローなエンドポイントやジッターによる遅延を除外するために、以下の実験を行いました。
- クッキーがない場合:無効なリクエスト
- 署名の一部(1 バイト)が反転した場合:無効なリクエスト
- 有効な署名だがフローエントリがない場合:無効なセッション状態
- 有効な署名で実在のフローエントリを使用した場合:Google のトークン交換に到達するが、偽造された認証コードにより後段で
エラーinvalid_grant - 有効な署名で Sleep ペイロードを使用した場合:指定した秒数だけ正確に遅延
影響範囲
これは単一のアプリケーションではなく、共有されている DevSite プラットフォームの一部です。したがって、
developers.google.com から取得された鍵を用いて、以下のドメインでも有効なセッションが署名生成・検証されていました。
firebase.google.comdeveloper.android.comsource.android.comdocs.partner.android.com
修正対応
この問題に対処するには、以下の 4 つの変更点が必須であり、これらはいずれも相互に関連しています。
- 露出された OAuth クライアントシークレットの両方をローテーションし、それらの鍵で署名されたすべてのセッションを終了させること。
- 各デプロイに対して、サーバー側のストレージから得た高エントロピーを持つ Flask
を割り当てる。 この鍵は OAuth の認証情報とは無関係で、用途も明確に区別されるべきです。SECRET_KEY - OAuth フローオブジェクトをサーバー側に保持する。 クッキーには不透明な参照のみを含み、機密情報は格納しないようにすべきです。
- クライアント境界を越えるデータには Pickle を使用しない。 スキーマチェック付きのデータ専用の形式に置き換えることで、偽造されたクッキーは無力化されます。
これらの中の一つだけでも攻撃連鎖を断ち切ることができましたが、実際にはすべての実施により、2 つの GET リクエストで Google のサーバー上でコードを実行できてしまいました。
タイムライン
| 日付 | 時刻 | イベント |
|---|---|---|
| 2026 年 8 月 6 日 | 22:48 | 報告書提出 |
| 2026 年 8 月 7 日 | 08:12 | 優先度判定(Triage)完了 |
| 2026 年 8 月 7 日 | 16:23 | 報告の受理 |
| 2026 年 8 月 27 日 | - | 賞与決定:90,000 ドル (通常の Google アプリケーション T2 カテゴリ、S0 レベル RCE 基準額 75,000 ドル × 卓越した報告品質に対する 1.2 倍の調整) |
| 2026 年 10 月 7 日 | 15:20 | 修正完了 |
Google Bug Hunters チームならびに DevSite エンジニアの皆様に、調査と迅速な修正にご尽力いただきました。感謝申し上げます。