CONTINUATIONフレームの深淵:HTTP/2ヘッダー分割がもたらす光と影、そして現場のアーキテクトが知るべき防衛策
Webの高速化と効率化の切り札としてHTTP/2が標準化されて久しい。単一のTCPコネクション上で複数のリクエストとレスポンスを多重化(マルチプレクシング)し、HOL(Head-of-Line)ブロックを克服したそのアーキテクチャは、現代のWebインフラストラクチャの基盤だ。
しかし、プロトコルの仕様が複雑化するにつれ、パケットのワイヤーフォーマットの深部には、設計者の苦渋の決断と、それに伴うセキュリティ上のダークサイドが潜むことになった。
今回は、HTTP/2のヘッダー圧縮(HPACK)とフレーム構造の隙間に存在する「CONTINUATIONフレーム」にスポットを当てる。パケットレベルの挙動から、TCP/TLSとのインタラクション、そして近年インフラ業界を震撼させた脆弱性「HTTP/2 Continuation Flood」のメカニズムと対策まで、ネットワークスペシャリストの視点から徹底的に解き明かしていく。
—
1. なぜCONTINUATIONフレームが必要なのか?(HPACKとフレーム境界のジレンマ)
HTTP/2の最大の発明の一つが、HPACKによるヘッダー圧縮だ。冗長なHTTPヘッダー(`User-Agent`, `Cookie`, `Accept-Encoding`など)を静的・動的テーブルを参照してインデックス化し、極限までペイロードサイズを切り詰める。
だが、ここに一つの物理的な矛盾が生じる。
HTTP/2のフレームは、標準では最大$2^{14} – 1$オクテット(約16KB)のペイロード長制限(`SETTINGS_MAX_FRAME_SIZE`)を持っている。通常、これほどのサイズがあればリクエストヘッダーが収まりきらないことはない。しかし、現代のWebアプリケーション、とりわけOAuth/OIDCのIDトークンや大量のCookieを抱えた巨大なリクエストヘッダーは、時に16KBを優に超える。
ここでRFC 7540(HTTP/2)の設計者が直面したジレンマがある。
- ルールA: `HEADERS`フレームは、単一の論理的なヘッダーブロックを運ばなければならない。
- ルールB: フレームサイズには上限がある。
この矛盾を解決するために生み出されたのが、`CONTINUATION`フレーム(Type: 0x9)である。
パケットのワイヤーフォーマット上の挙動
もしヘッダーサイズが`SETTINGS_MAX_FRAME_SIZE`を超える場合、クライアントは以下のようなフレームシーケンスをワイヤー上に流す。
1. `HEADERS`フレーム: 末尾のフラグ(`END_HEADERS`)を立てずに送信する。
2. `CONTINUATION`フレーム群: 収まりきらなかったヘッダーの断片を、同じストリームIDに向けて連続して送信する。
3. 最後の`CONTINUATION`フレーム: 末尾に`END_HEADERS`フラグ(`0x4`)を立てて、ヘッダーブロックの終端を宣言する。
+———————————————–+
| Length (24) |
+—————–+————-+—————+
| Type (0x9) | Flags |
+—————–+————-+—————+
|R| Stream ID (31) |
+—————–+—————————–+
| Payload: |
| Header Fragment (Variable)… |
+———————————————–+
ネットワークアナライザ(Wiresharkなど)でこの瞬間をキャプチャすると、1つのHTTPリクエストのメタデータが、まるでムカデのように複数の`CONTINUATION`フレームに分断されてTCPセグメントに詰め込まれている様が確認できる。この挙動自体は、プロトコル仕様に忠実な美しい断片化処理である。
—
2. トランスポート層・TLSハンドシェイク最適化との親和性
この「ヘッダーの分割と連続送信」という挙動は、TCPおよびTLS(特にTLS 1.3)の挙動と密接に絡み合っている。
TCPウィンドウとスライディングウィンドウの罠
`CONTINUATION`フレームが発生するような巨大なヘッダー(例えば、32KBのCookieを持つリクエスト)を送信する場合、アプリケーション層のバッファからカーネルのソケットバッファ(`SO_SNDBUF`)への書き込みが複数回に分かれる。
ここでTCPの初期輻輳ウィンドウ(initcwND)やNagleアルゴリズムの制御が不適切だと、以下のようなレイテンシペナルティが発生する。
1. `HEADERS`フレームが最初のTCPセグメントとして送出される。
2. 受信側からのACKを待つ、あるいはTCPセグメントのMSS(Maximum Segment Size)の制約により、次の`CONTINUATION`フレームが次のRTT(Round Trip Time)まで持ち越される。
結果として、「ヘッダーが揃うまでHTTPリクエストの処理(バックエンドへのフォワード)を開始できない」というHTTP/2のサーバー側実装において、TTFB(Time to First Byte)の増大を招く原因となる。
TLSレコード層とのアライメント
TLS 1.3上でHTTP/2を動かす場合、各フレームはTLSレコードにカプセル化される。
通常、OpenSSLなどの暗号化ライブラリは、効率的なパディングとAEAD暗号(AES-GCMやChaCha20-Poly1305)の認証タグ付与のため、複数のHTTP/2フレームを1つのTLSレコードにまとめようとする(あるいはその逆)。
`CONTINUATION`フレームが細切れに生成されると、TLS層でのレコード断片化や、CPUの暗号化処理オーバーヘッドが増加する。高スループットを要求されるAPIゲートウェイ(EnvoyやNginxなど)では、このフレームの粒度がCPUキャッシュ効率やコンテキストスイッチの頻度に微妙な影を落とす。
—
3. 2024年の衝撃:「HTTP/2 Continuation Flood」の脅威
しかし、この`CONTINUATION`フレームの仕様こそが、近年発見された最も深刻な脆弱性クラスの一つ、「CVE-2024-27919」をはじめとするHTTP/2 Continuation Floodの温床となった。
セキュリティ専門家やインフラエンジニアにとって、この脆弱性は「プロトコルの善意がどのように武器になり得るか」を示す教科書的な事例だ。
攻撃のメカニズム
攻撃者は、次のような悪意あるリクエストを構築してサーバーに送りつける。
1. 新しいストリームを開始し、`HEADERS`フレームを送信する(この時点では`END_HEADERS`は偽)。
2. その後、際限なく無数の小さな`CONTINUATION`フレームを送り続ける。それぞれのフレームには`END_HEADERS`フラグを立てない。
3. 攻撃者は、`END_HEADERS`を送らないまま、何百万ものストリームをこの状態に維持する。
サーバー内部での悲劇
多くのHTTP/2サーバー実装(NGINX, Apache, Envoy, Node.jsなど)は、`END_HEADERS`を受信するまで、受信したヘッダーフラグメントをメモリ上のバッファに蓄積し続けなければならないという設計になっていた。
- サーバーは「まだヘッダーが続いている」と解釈するため、メモリを解放できない。
- ストリーム多重化の恩恵を受けようと、数千・数万のストリームでこれを同時にやられると、サーバーのヒープメモリが瞬時に枯渇する。
- CPUも、終わりなきフレームのデコードとHPACKコンテキストの維持(あるいは未完成のHPACKステートマシンの処理)に奪われ、CPU使用率が100%に張り付いた挙句、OOM(Out of Memory) Killerによってプロセスがクラッシュする。
従来のSlowloris攻撃がTCP層のコネクション枯渇を狙ったのに対し、CONTINUATION Floodはアプリケーション層のパーサーとメモリ管理の隙を突いた、極めて洗練されたDoS攻撃であった。
—
4. 現場のアーキテクトが講じるべき実装上の防衛策とチューニング
この脆弱性の発覚を受け、主要なWebサーバーやリバースプロキシのコミュニティはパッチを急ピッチでリリースした。しかし、インフラを預かるテックリードとしては、ソフトウェアのアップデートだけに頼るのではなく、プロトコルの挙動を深く理解した上での防御的設定が不可欠だ。
ここでは、実践的な防衛策とパラメーターチューニングの指針を示す。
A. リバースプロキシ(例: NGINX / Envoy)の適切な制限設定
サーバー側で「1つのヘッダーブロックに許容する最大サイズ」および「CONTINUATIONフレームの連続回数」に厳格なしきい値を設ける。
Nginxの場合(メインライン系での対応)
Nginxでは、HTTP/2の内部バッファリング制限を適切に管理する必要がある。最新のパッチ適用はもちろん、以下のディレクティブを通じて異常なリクエストを早期に弾く。
http {
# HTTP/2のリクエストヘッダーバッファサイズを制限
# デフォルトから肥大化させすぎないことが、Continuation Flood対策の第一歩
client_header_buffer_size 4k;
large_client_header_buffers 4 16k;
server {
listen 443 ssl http2;
# 悪意ある巨大なCONTINUATIONフレームの連鎖をブロックするため、
# サーバー側で処理可能な最大ヘッダー長を厳しく制限する
# (※OAuth等の巨大トークンを通す場合は、アプリケーション設計側の見直しが必要)
}
}
Envoy Proxyの場合
Envoyでは、モジュールごとにHTTP/2の各種上限値を細かくチューニングできる。`max_concurrency`や、ヘッダー長に関するサーキットブレーカーを設定する。
EnvoyのHTTP/2設定例(YAML)
static_resources:
listeners:
- name: ingress_listener
address:
socket_address: { address: 0.0.0.0, port_value: 443 }
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
“@type”: type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: ingress_http
http2_protocol_options:
# 1つのストリームで許可する最大ヘッダー長を制限
max_headers_size: 65536 # 64KBを超えるヘッダーは拒絶
# 同時ストリーム数の上限を適切に絞り、メモリ枯渇を防ぐ
max_concurrent_streams: 100
B. アプリケーション設計のシフト(巨大CookieやJWTの排除)
そもそも、なぜ`CONTINUATION`フレームが頻発するような巨大なヘッダーが生成されるのか? その根本原因を断つことが、アーキテクトとしての本質的な解決策だ。
1. Cookieのダイエット: セッション情報をすべてCookieに詰め込むアンチパターンを避け、サーバー側のセッションストア(Redisなど)に逃がし、CookieにはセッションID(32バイト程度)のみを持たせる。
2. JWTの肥大化対策: IDトークンに過剰なクレーム(権限リストやユーザープロファイル全体)を含めず、必要な最小限の識別子のみに絞るか、パブリックキーの参照(JKU/KID)を活用する。
—
5. おわりに:プロトコルの「裏側」を愛するということ
HTTP/2の`CONTINUATION`フレームは、一見すると「大きなヘッダーを安全に運ぶための地味なユーティリティ」に過ぎない。しかし、その仕様の裏側には、可変長フレームの設計思想、HPACKのコンテキスト管理、そしてパケットの断片化がもたらすセキュリティ上のアキレス腱が隠されている。
ネットワークスペシャリストやインフラエンジニアにとって、ミドルウェアが吐き出すエラーログや、パケットキャプチャのタイムラインの向こう側にある「なぜその仕様になったのか」「そこをどう悪用し、どう守るのか」という文脈を想像する力こそが、障害を未然に防ぎ、極限のパフォーマンスを引き出す唯一の武器となる。
「動けばいい」の境界線を越え、プロトコルの鼓動を聞き分けるエンジニアリングを、これからも続けていこう。
コメント