要約

  • Cloudbleed では HTML 変換処理がバッファの外側を読み、無関係なリクエストの断片を応答に混入させた。共有エッジに期待される顧客間の境界が破れた事故だった。
  • Cloudflare は機能単位の停止と解析器の修正で新たな漏出を止められたが、外部キャッシュの断片と露出した可能性のある認証情報には、パッチとは別の回復証明が必要だった。

引き金となったサイトと被害側は一致しない

2017年2月17日、Google Project Zero は Cloudflare に、同社経由で配信されたページへ本来そこにない断片が現れる現象を報告した。Cookie、認証トークン、POST 本文など、プロセスメモリから得られたデータであり、一部の応答は検索エンジンにも保存されていた。

Cloudbleed という通称は Heartbleed を連想させる。ただし、ここで重要なのは共有基盤の構造だ。多数の顧客のページを変換する同じエッジプロセスで不具合が起きた。異常な HTML を持つサイトが欠陥を作動させても、その応答へコピーされた隣接バイトは別の顧客のリクエストに由来し得た。

したがって、トリガーとなったドメインの一覧を被害者名簿として扱うことはできない。壊れた経路を呼び出したサイトと、偶然その近くにデータが置かれたサイトは別の役割を担う。対象機能を利用していない顧客でも、誤った瞬間に同じプロセスを共有すれば境界を越えられた可能性がある。

破られた条件は単なる「このページを正しく解析する」ではない。「あるテナントの処理が、別のリクエストのメモリを読まず、送信しない」という隔離条件である。共有ワーカーやバッファは効率を生むが、その条件が失われれば、無関係な信頼領域を接触させる経路になる。

新しいバッファリングが古いポインタ不具合を表面化させた

Cloudflare は原因を、Ragel で生成された古いコードに突き止めた。メールアドレス難読化、Server-Side Excludes、Automatic HTTPS Rewrites という三つの HTML 変換機能がこのコードを使っていた。fholdが一つ欠けていたため、ポインタがバッファ末尾に残り、呼び出し側が有効な範囲だと解釈して境界外までコピーすることがあった。

漏出が急増したのは、新しいcf-html解析器の展開後だった。バッファの扱いが変わり、旧コードが境界条件に遭遇しやすくなった。ただし Cloudflare は、不具合が Ragel そのものやcf-htmlそのものにあったのではないと説明している。新部品が古い前提を変え、潜伏していた欠陥を高頻度の漏出へ変えたのである。

この種の変更は、部品だけをレビューしても足りない。新コードが正しいかに加え、周囲の旧コードが依存していた暗黙の前提を何が壊すかを調べる必要がある。安全な展開には、バッファ境界での差分試験、応答の出所を確かめるカナリア、そして新部品によって再び通るようになった旧経路のクラッシュ分析が要る。

絶対時刻で確認する停止順序

最初の回復手段は Cloudflare の内部にあった。新しいデータを送り出す機能経路を消すことである。事故報告の詳細な UTC 時刻では、2月18日00時32分に不具合の詳細を受領し、01時19分にメールアドレス難読化を全世界で停止、04時24分に Automatic HTTPS Rewrites を停止、07時22分にcf-html停止スイッチを含むパッチを全世界へ展開した。Server-Side Excludes には独自の全体停止スイッチがなく、その場で実装する必要があった。

停止スイッチは運用上の便利機能ではなく、機能の安全設計に含まれる。スイッチがなければ事故の最中にコード変更が必要になる。可逆な機能なら、無関係なプロキシ機能を残したまま自分だけを切り離せる。不可分な機能は、広い停止か継続漏出かという悪い二択を作る。

Cloudflare は、TLS 終端が別の NGINX インスタンスに隔離されていたため、顧客の SSL 秘密鍵はこの経路で漏れていないと報告した。ただし HTTP プロセスには、Cloudflare 機器間暗号化に使う秘密鍵と少数の内部認証秘密が存在した。プロセス分離は重大な一種類を守ったが、到達可能な秘密をゼロにはせず、影響範囲を狭めたのである。

後の再実装では Rust が採用された。安全な Rust コードでは範囲外メモリアクセスが言語の検査で抑止されるため、レビューで全ポインタミスを探すより強い局所条件になる。ただし Rust も、認可の誤り、不安全な外部コード、不適切なデータフローまで自動的に直すものではない。

推計値は完全な名簿ではない

初期報告は、最も露出が大きかった時期を2月13日から18日とし、約330万リクエストに一件、比率にして約0.00003%が漏出し得たと推計した。後の分析では、2016年9月22日から2017年2月18日までのトリガーを1,242,071件と推計した。2月13日より前は605,037件でトリガーサイトは180未満、最後の五日間は637,034件で6,457サイトだった。

数字には証拠の限界を添えなければならない。詳細なリクエストログは1%の標本で、2月8日から18日分しかなかった。それ以前の詳細ログは削除済みで、古い期間は集約分析とクラッシュ記録から推計された。Cloudflare が調査した記録には悪用の証拠がなかったが、それは有限の記録内での観察であり、悪用が一度もなかった証明ではない。

トリガー数は被害者数ではない。全トラフィックに対する低い比率は、一つの断片が持つ機密性を表さない。標本ログで痕跡が見つからないことも、過去を確定するものではない。

パッチは送信済みのバイトを呼び戻せない

三機能の停止は新たな汚染応答を止めた。しかし、配信済みのページ、検索インデックス、コピー済みの認証情報は撤回できない。Cloudflare は、推計トリガーの半数超が検索クローラーによるものだったこと、8万件を超える固有のキャッシュページを削除したこと、第三者キャッシュで150顧客に関連するデータを発見したことを報告した。

これらは完全な被害者数を示さない。削除ページすべてに機密情報があったとは限らず、150顧客は上限ではない。キャッシュ削除も私的なコピーの不存在を証明しない。止血と回復は終了条件が異なるのである。

解析器の修正が証明できるのは、既知経路が試験条件で新たな漏出を生まないことまでだ。保存済み断片はキャッシュ管理者が別に削除する。顧客は、越境した可能性のあるセッション、トークン、API 秘密、パスワードを選び、各利用先で失効または交換する必要がある。新しい文字列の発行ではなく、古い権限の拒否が完了証拠になる。

小さな共通境界と、可逆な局所判断

Lu Heng の Running-Code Primacy に従えば、ステータスページの「解決済み」は証明にならない。稼働中のエッジが越境メモリを返さず、機能状態が観測でき、旧認証情報が以前使えた場所で拒否され、残存キャッシュが実際の管理者によって調査・削除されて初めて各層の結果が示される。

Minimum Initial Specification が求める共通条件は小さく絶対的でよい。応答に含められるのは、その応答に認可されたデータだけである。任意の変換、解析器実装、展開時期、停止判断は局所に残す。すべての顧客へ同じ機能リスクを強制せず、隔離条件を弱めずに採用、拒否、変更ができる。

Cloudbleed が否定したのは共有基盤ではない。共有には硬い膜が必要であり、その膜を通る任意機能には、サービス全体より先に自分だけを消せる手段が必要だと示したのである。

出典