- HTTP/2 CONTINUATION Flood は、
END_HEADERS なしでヘッダーフレームを送り続けることでサーバーの可用性を損なえる、HTTP/2実装上の脆弱性群である
- 攻撃リクエストは完了しないため HTTPアクセスログ に残らず、原因特定には生トラフィックのバイト解析が必要になる場合がある
- 実装によって影響範囲は異なり、CPU枯渇、複数接続によるOOM、単一接続によるOOM、切断タイミングのバグによるクラッシュまで起こりうる
- Go、Firefox、Node.jsの事例では、それぞれHPACKデコードの継続、レスポンスヘッダーサイズ制限の欠落、
CONTINUATION 処理中の切断とメモリカウンター更新の競合が明らかになった
- Rapid Resetと異なり、多くの実装では 単一のTCP接続 だけでサーバークラッシュが可能であり、HTTP/2を使う広範なインターネットサービスが影響を受けうる状況だった
HTTP/2で CONTINUATION フレームが使われる仕組み
- HTTP/2はHTTP/1.1のようにテキスト行をやり取りするのではなく、バイナリフレーム を交換するプロトコルである
HEADERS フレームはリクエストとレスポンスのHTTPヘッダーを送り、ヘッダーデータは HPACK でエンコードされたfield block fragmentに格納される
HEADERS フレームにはヘッダーとストリームの終端を示すフラグがある
END_HEADERS: そのフレームが送るべき全ヘッダーを含んでいることを示す
END_STREAM: リクエストまたはレスポンス本文がもう存在しないことを示す
- フレームには通信開始時に決まる 最大サイズ があり、受信フレームが許容サイズを超えるとプロトコルエラーで接続が切断される
- 単一の
HEADERS フレームに全ヘッダーを収められない場合、END_HEADERS のない HEADERS の後に CONTINUATION フレームが続く
END_HEADERS のない HEADERS フレーム
END_HEADERS のない追加の CONTINUATION フレーム群
- 最後の
CONTINUATION フレームに END_HEADERS を設定
- 最後のヘッダーフレームの後には、リクエストデータを含む
DATA フレームが来るか、HTTP/2ストリームが終了する
脆弱性の核心: 終わらないヘッダーストリーム
- クライアントが新しいHTTP/2ストリームを開始した後、
HEADERS と CONTINUATION フレームを送りつつ END_HEADERS を一切設定しなければ、サーバーは 無限のヘッダーストリーム を延々と解析・保存しようとする
- HTTP/1.1サーバーには通常、無限ヘッダーを防ぐための2つの仕組みがある
- ヘッダー一覧が許容サイズを超えたら接続を切る ヘッダーサイズ制限
- リクエストやヘッダーが時間内に送信されなければ接続を切る リクエスト/ヘッダータイムアウト
- 多くのHTTP/2実装ではこうした保護が欠けているか、誤って実装されており、Apache httpd、Envoy、複数のHTTP/2パッケージやコーデックも含まれていた
- 実装ごとの結果は大きく4種類に分かれる
- CPU枯渇: 追加ヘッダーを読み取りデコードする間にCPU使用率が上がり、他のリクエストへの応答が遅延または停止する
- 複数接続ベースの OOM:
CONTINUATION ヘッダーがメモリに保存され、ヘッダー一覧サイズ制限はあってもヘッダータイムアウトがないため、各接続がメモリを占有し続ける
- 単一接続ベースの OOM: 一部実装ではメモリが尽きるまでヘッダーを読み続け、OSがプロセスを終了させる
- 数フレームでのクラッシュ:
CONTINUATION ストリームの途中で接続が切れた際、実装バグによってサーバーがクラッシュする
END_HEADERS がないとリクエストが正しく閉じられないため、悪意あるクライアントからのリクエストはアクセスログに記録されない
Goの事例: HPACKデコードが止まらないCPU枯渇
- Goは
CONTINUATION Floodにおいて CPU枯渇 が顕著だった事例である
- Go実装は
http2MetaHeadersFrame という抽象化によって、1つの HEADERS フレーム、0個以上の CONTINUATION フレーム、HPACKデコーダーをひとまとめにしている
readMetaFrame はヘッダーサイズ制限に達するかエラーが発生すると、SetEmitEnabled(false) を呼び出してデコード済みヘッダーの出力を止める
- しかしヘッダー出力を止めた後でも、HPACKデコーダー は入力バイトのデコードを継続する
- フレーム供給ループは
HeadersEnded() が true になるまで止まらず、これは END_HEADERS フラグが設定されたときにのみ起きる
- 攻撃者が
END_HEADERS を送らなければ、readMetaFrame は返らず、攻撃者が送り続ける限りHPACKデコーダーは新しいバイトを処理し続ける
OOM事例とFirefoxクライアントへの影響
- OOMは、
CONTINUATION フレームで形成されるヘッダー一覧サイズを制限していない実装で発生した
- ヘッダータイムアウトのない実装では、単一のHTTP/2接続 だけでサーバーをクラッシュさせることができた
- idle timeoutがある実装でも、複数のHTTP/2接続に接続ごとの上限近くまでRAMを占有させたうえで、最後の
CONTINUATION フレームを数秒おきに1バイトずつ送って接続を維持する手法が可能だった
CONTINUATION Floodはサーバーだけでなく、ブラウザーのような クライアント側 でも発生しうる
- Mozilla Firefoxの修正コミットでは、集計済みヘッダーサイズと新規フレームサイズの合計が
network_http_max_response_header_size() を超えた場合に PROTOCOL_ERROR としてセッションエラーを返すチェックが追加された
Node.jsの事例: 切断中のassertionクラッシュ
- Node.jsは無限の
CONTINUATION フレームストリーム自体は適切に処理していたが、ヘッダーストリームの途中で接続が切れると data race が発生した
- 攻撃コードの実行中、Node.jsは
Http2Session::~Http2Session() における CHECK_EQ(current_nghttp2_memory_, 0) assertion失敗でクラッシュした
- クラッシュは、HTTP/2クライアントがNode.jsサーバーから切断される正確なタイミングと結びついており、そのassertionは
Http2Session デストラクター内にあった
- Node.jsはHTTP/2接続処理のために nghttp2 ライブラリを組み込んでいる
current_nghttp2_memory_ はnghttp2内部で確保されたメモリを追跡し、デストラクターの session_.reset() 後にすべてのnghttp2アーティファクトがメモリから除去されたかを確認する
- 調査の結果、
CONTINUATION フレーム解析中にnghttp2コールバックと reset() が同時に実行される場合があることが分かった
NGHTTP2_IB_EXPECT_CONTINUATION 状態で CONTINUATION フレームが到着する
- 状態が
NGHTTP2_IB_READ_HEADER_BLOCK に変わる
session_after_header_block_received、session_call_on_frame_received、on_frame_recv_callback の流れが続く
- Node.jsの
OnFrameReceive と HandleHeadersFrame でメモリカウンターが更新される
HandleHeadersFrame と Http2Session::~Http2Session() が同時に実行されると current_session_memory_ が競合して更新され、current_nghttp2_memory_ の値が負になって CHECK_EQ が失敗する
2019年のHTTP/2脆弱性との違い
- 2019年にNetflixとGoogleが報告したHTTP/2脆弱性群は、CERT/CC Vulnerability Note VU#605641 に整理されている
- CVE-2019-9516「0-Length Headers Leak」は、長さ0のヘッダー名と値によって一部実装がメモリを確保し、セッション終了まで保持してしまう問題である
CONTINUATION Floodは空ヘッダーではなく、サーバーが設定したフレームサイズ上限まで 大量のランダムヘッダー を送る手法である
- CVE-2019-9518「Empty Frame Flooding」は、空のpayloadを持つ
DATA、HEADERS、CONTINUATION、PUSH_PROMISE フレームなどをend-of-streamなしで送り、相手に攻撃帯域幅に比して過剰な処理時間を使わせる問題である
CONTINUATION Floodは空フレームではなく、可能な限り大きなフレームを使ってメモリを占有し、デコード処理でCPUサイクルを消費させる
Rapid Resetより深刻になりえた理由
- 2023年10月、HTTP/2プロトコルのzero-dayである「Rapid Reset」の詳細が公開され、「史上最大のDDoS攻撃」とも呼ばれた
- Rapid Resetは
END_STREAM と END_HEADERS が設定された HEADERS フレームと RST_STREAM フレームの組み合わせを利用する
- この手法ではrate limitingのような標準的な緩和策で被害を軽減でき、サーバー管理者も大量の受信リクエストをログで確認して警告を受けられる
CONTINUATION Floodでは END_HEADERS がないため、1件のリクエストすら完了せず、管理者はログ上でリクエストを確認できない
- 多くの実装では
CONTINUATION Floodは単一のTCP接続だけでサーバークラッシュが可能であり、一部の事例ではごく少量のデータだけでも可能だった
- Rapid ResetはDDoS攻撃に使われ、ほとんどの場合、攻撃成功にはbotnetが必要だった
インターネットサービスに及びえた影響
- Cloudflare Radar によれば、HTTP/2トラフィックはボットを除く人間のHTTPトラフィックの約 60% を占める
- Cloudflare Radarは、HTTPトラフィックがインターネット全体の転送量の70%以上を占めると推定している
- 影響を受けるプロジェクトの重要性と悪用の容易さを考えると、インターネットのかなり大きな部分がこの脆弱性にさらされていた
- HTTPはWebサイトだけでなく、多くの RESTful API にも使われている
- 重要な企業・政府APIやWebサイトの可用性問題は、数百万ドル規模の損失や混乱を引き起こしうる
- 悪用されていた場合、サーバー管理者がHTTP/2の知識なしにデバッグするのは非常に難しかった可能性がある
- 悪意あるHTTPリクエストが正しく閉じられない
- サーバーアクセスログにリクエストが表示されない
- ほとんどのHTTP/2サーバーには高度なフレーム解析機能が不足している
- 手作業で生の接続データを解析しなければならない
協調公開とCERT/CCの対応
- この脆弱性群は インターネットの安全性 に対して相当なリスクを持っていた
- 2024年1月の報告後、CERT/CC はこの問題を追跡するためVulnerability Coordination caseを開設した
- 複数の大手技術企業とオープンソースプロジェクトが、関連する責任ある公開プロセスに参加した
- 単独の研究者が多数の実装をすべて点検するのは難しいため、複数ベンダーに影響する問題にはVulnerability Coordinationが必要だった
- CERT/CCはこの問題に関する Vulnerability Note を公開しており、この種のノートは毎年わずかしか公開されない
1件のコメント
Hacker News のコメント
先月、Banditでまさにこの問題を緩和していた
https://github.com/mtrudel/bandit/blob/main/lib/bandit/http2...
実装者の立場からすると、正直あまりにも当然に防ぐべき部分。ずっと前から気にしていて、他の実装も当然防御しているものだと思っていた
この数か月で数十の実装を確認したが、不思議なことに主要なHTTP/2サーバーでさえ、こうした保護がなかったり誤って実装されていたりした
本質的には、何でも自動で動的に拡張することに慣れ、サイズがどこまで大きくなり得るかを気にしない開発文化が生んだ結果だと思う
この種の問題はHTTP/2に限ったものではないが、HTTP/2の極端な複雑さが一役買った可能性は大きい。HTTP/1.xの時代にはCのような言語に慣れた開発者が多く、バッファ長の管理を常に意識していたし、リクエスト全体でヘッダー割り当ては多くても数KBあれば十分なのに、無限に増えるようにはしなかったはず
slowlorisやクエリパラメータのハッシュ衝突のような多くのサービス拒否攻撃は、制限されたリソース使用量を後になってようやく考慮したことで現実化した
> 影響なし: Nginx, Jetty, HAProxy, NetScaler, Varnish. [0]0: https://nowotarski.info/http2-continuation-flood/
少なくとも、満杯でないHEADERSフレームの後では禁止する案だけでも受け入れられていれば、もっと堅牢だっただろうが、エンコード作業自体が難しくなり得ると考えられていた。圧縮器のバイト境界のような問題があるためだ
10年ごとに同じものを「再発見」している様子は笑える。最近ではよく知られたRESET_STREAM floodで、今回はCONTINUATION、次は長さ0のDATAフレーム、1バイトのWINDOW_UPDATE、CPUを大量に使わせるINITIAL_WINDOW SETTINGSあたりが来そうだ。既知の問題に名前と、できればロゴさえ付けられれば、このセキュリティ・サーカスは回り続ける
同じ著者が影響を受けるWebサーバー/リバースプロキシをまとめた以前の記事
https://nowotarski.info/http2-continuation-flood/
この記事は一日中トップにあった
気になるのだが、トラフィックの少ないWebサイトなら、単にHTTP/1.1で運用する方が安全ということもあるのだろうか?
HTTP/2とHTTP/3は機能面で大きく異なる。多重化、ウィンドウ制御、HPACKなどが追加され、HTTP/1.1のほぼステートレスな接続がステートフルな接続に変わる。ステートフルな接続を維持するには状態や設定といったデータを保存する必要があり、だからこうした問題が生じる
HTTP/2では多重化が追加されるため、防御上の特性も変わる。たとえば接続がCDNのオリジンリクエストから来るなら、接続数は少なく許可し、各接続に大きな多重化チャネルのプールを持たせられるが、ユーザーが直接アクセスする場合は、接続数は多く許可しつつ各接続の多重化チャネル数は減らしたいかもしれない。HTTP/1ではほとんどすべてが似たように見えていたので、防御はずっと単純だった
HTTP/1には目立ちにくい境界条件や古い例外動作が多い。テキスト形式は有効なヘッダーだけを見ているときよりはるかに柔軟で、複数行ヘッダー、古いMIME機能、100-continueの競合状態、ユーザー定義のホップバイホップヘッダー、GET本文のような曖昧な機能もある
幸い、新しいHTTP RFCは多くの落とし穴を文書化している。RFC 2616だけを見て実装しても、安全な実装にはならない
リクエストやレスポンスの実際のサイズは、複数の方法で同時に指定でき、値が衝突することもある。また、複数の機能の組み合わせや、後方互換性のために奇妙なパース規則が必要なヘッダー値にも左右されるため、「単純な」HTTP実装はリクエストスマugglingにだまされる可能性がある
どちらにせよ、堅牢で十分にテストされた成熟した実装が必要だ
一般的な静的アセット配信では、HTTP/1がドメインごとの接続数制限を受けるため、より多くのアセットを並列に読み込める点だけが利点だ。別ドメインのCDNを使えば、通常この問題も回避できる
理論上はHTTP/2でバンドルしていないJavaScriptアセットを配信できるが、実運用環境では見たことがない。多くの場合、依然としてコンパイル段階が必要だからだろう
これをゆっくりやればslowloris v2と呼べそうだ :(
HTTP/2、あるいはトランスポート層の「アップグレード」をアプリケーション層プロトコルに無理やり押し込む方法