【テクニカル・上級編】HTTP/2におけるセキュリティ脆弱性:ストリーム多重化攻撃 – HTTPプロトコル・通信規格実践ガイド

HTTP/2マルチプレクシングの光と影:ストリーム多重化攻撃のメカニズムと、極限のチューニング戦略

インターネットの歴史において、HTTP/1.1の「Head-of-Line(HoL)ブロッキング」は長年の呪縛だった。1つのTCPコネクション上でリクエストを直列に処理せざるを得ず、ブラウザはドメインごとに複数のTCPコネクションを張ることで、その不条理な遅延を力技で回避してきた。

そこで登場したHTTP/2は、単一のTCPコネクション上に「ストリーム(Stream)」という仮想的な双方向チャネルを多重化し、フレーム(Frame)単位でバイナリデータをインタリーブさせることで、この呪縛を鮮やかに解き放った。パケットレベルの効率は劇的に向上し、現代のWebインフラストラクチャの基盤として君臨している。

しかし、プロトコルの高度化は、常に新たな攻撃サーフェス(攻撃対象領域)を生み出す。マルチプレクシングの核心である「ストリームの並行処理」は、一歩設計を誤れば、サーバーを容易に膝をつかせる凶悪な脆弱性へと変貌する。

今回は、HTTP/2の内部挙動、HPACKとTLSの密接な関係、そして現場のインフラエンジニアが直面する「ストリーム多重化攻撃(Stream Multiplexing Attack)」の正体と、その処方箋について深く掘り下げていこう。

—

1. パケットが織りなす挙動:HTTP/2フレームとストリームの深層

HTTP/2の美しさは、そのバイナリフレーミングレイヤーにある。TCPセグメントのペイロードとして流れるデータは、すべて「長さ」「タイプ」「フラグ」「ストリーム識別子」を持つプレフィックス(9バイト)と、それに続くペイロードで構成される。

+———————————————–+
| Length (24) |
+——————————-+—————+
| Type (8) | Flags (8) |
+-+——————————-+————-+
|R| Stream Identifier (31) |
+-+———————————————–+
| Frame Payload |
| … |
+———————————————–+

ここで重要なのは、ストリーム識別子(Stream Identifier)の存在だ。クライアントが初期化するストリームは奇数、サーバーは偶数を使用し、単一のTCPコネクション上で最大 $2^{31}-1$ 個のストリームを同時にハンドリングできる。

ブラウザが1つのHTMLを要求し、その中で数百個の画像やスクリプトを読み込むシーンを想像してほしい。HTTP/2では、これらすべてのリクエストが単一のTCPコネクション上で `HEADERS` フレームおよび `DATA` フレームとして細切れに送信され、サーバー側で非同期に処理される。

HPACKによるヘッダー圧縮の罠

このマルチプレクシングを支えるもう一つの主役が、静的テーブルと動的テーブルを用いたヘッダー圧縮アルゴリズム「HPACK」だ。
同じコネクション内であれば、繰り返送信されるHTTPヘッダー(例: `user-agent`, `accept-encoding` など)を数バイトのインデックス参照に置き換えて送信できるため、RTT(Round Trip Time)が厳しい環境でも帯域を極限まで節約できる。

しかし、このHPACKの動的テーブル(Dynamic Table)こそが、のちにセキュリティインシデントの温床となる。動的テーブルは接続ごとに状態を維持(Stateful)するため、パケットロスや順序逆転に対する耐性を保ちつつ、メモリ上で厳密に同期されなければならない。ここにリソース消費の非対称性が潜んでいる。

—

2. ストリーム多重化攻撃のメカニズム:なぜサーバーは沈黙するのか

悪意ある攻撃者がHTTP/2の仕様を逆用すると何が起きるか。代表的なものが「HTTP/2 Rapid Reset Attack(CVE-2023-44487)」に代表される、ストリーム多重化を悪用したDoS攻撃だ。

攻撃のシナリオ

1. 攻撃者はサーバーとの間でTCPおよびTLS(ALPNでh2をネゴシエーション)のハンドシェイクを完了させ、HTTP/2コネクションを確立する。
2. コネクション上で、何千、何万という膨大な数のストリームを同時にオープンする(`HEADERS` フレームの送信)。
3. サーバーがそのリクエスト処理のためにCPUやメモリを割り当て、バックエンドへのルーティングを準備した瞬間に、攻撃者は即座に `RST_STREAM`(ストリーム強制終了)フレームを送信する。
4. クライアント側から見ると、リクエストは発行されて即座にキャンセルされたため、ローカルのコストはほぼゼロである。
5. しかし、サーバー側では「リクエストの受信」「ストリームコンテキストの生成」「キャンセル処理の割り込み」という一連のライフサイクルが強制的に実行され、CPUとメモリ(特にカーネルのソケットバッファやアプリケーションのワーカースレッド)が確実に削られていく。

結果として、数台のボットから少量の帯域を送信するだけで、数百万QPSを処理できるはずの強靭なWebサーバーやリバースプロキシが、CPU使用率100%に張り付いて応答不能(リソース枯渇)に陥る。

—

3. 実践:極限のパフォーマンスとセキュリティを両立するチューニング

この脅威からインフラを守るためには、単にデフォルト設定のまま運用するのではなく、トランスポート層、TLS、そしてHTTP/2プロトコル層のパラメータを緻密にチューニングする必要がある。

以下に、NginxおよびLinuxカーネルにおける実践的な設定アプローチを示す。

Linuxカーネル層のTCPバッファチューニング (`/etc/sysctl.conf`)

HTTP/2は単一のTCPコネクションに多くのストリームを相乗りさせるため、TCPウィンドウサイズやバッファの枯渇が全体のパフォーマンスに直結する。

TCPの輻輳制御アルゴリズムにBBRを採用し、高スループットと低遅延を両立
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr

TCP受信/送信バッファの最大値を拡張し、多数のストリーム多重化に伴うデータ量を許容
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216

TIME_WAITソケットの再利用を有効化し、短命なコネクションの高負荷に対応
net.ipv4.tcp_tw_reuse = 1

NginxにおけるHTTP/2リソース制限と防衛策

HTTP/2の挙動を制御するディレクティブを適切に設定し、悪意あるストリームの乱立を防ぐ。特に `http2_max_concurrent_streams` の調整がキモとなる。

http {
# HTTP/2設定ブロック
server {
listen 443 ssl http2;
server_name example.com;

# SSL/TLSの最適化(TLS 1.3必須、強力な暗号スイート)
ssl_protocols TLSv1.3;
ssl_ciphers EECDH+CHACHA20:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256;
ssl_prefer_server_ciphers on;

# 同時ストリーム数の上限を厳しく制限(デフォルトは通常128程度だが、環境に応じて絞る)
# 攻撃者が無尽蔵にストリームを開くのを物理的にブロックする
http2_max_concurrent_streams 100;

# HPACK動的テーブルのサイズ制限(メモリ枯渇攻撃を防ぐ)
http2_max_field_size 4k;
http2_max_header_size 16k;

# 1つの接続あたりに許可するリクエスト数の上限を設定し、コネクションの占有を防ぐ
keepalive_requests 1000;

# タイムアウトの短縮化(アイドル状態の不正なコネクションを即座に切断)
client_body_timeout 10s;
client_header_timeout 10s;
keepalive_timeout 65s;

location / {
proxy_pass http://backend_cluster;
proxy_http_version 1.1;
# バックエンドとの接続維持設定
proxy_set_header Connection “”;
}
}
}

—

4. アーキテクトが心得ておべき「攻防一体」の視点

ネットワークアーキテクチャの設計において、「利便性(パフォーマンス)」と「安全性(堅牢性)」は常に背中合わせのトレードオフにある。HTTP/2はその優れたマルチプレクシングによってページのロード時間を劇的に短縮したが、同時に「1つのパイプラインが汚染されたときのダメージ」と「リソース制御の複雑性」を私たちに突きつけた。

脆弱性(CVE-2023-44487など)が発覚した際、多くの現場があわててパッチ適用や設定変更に追われたが、プロトコルの本質を理解していれば「なぜそのパラメータが必要なのか」「どのレイヤー(TCP、TLS、HTTP/2層)でブロックすべきなのか」が自ずと見えてくる。

次世代のHTTP/3(QUIC)では、UDPベースのトランスポートを採用し、TCPのHead-of-Lineブロッキングを完全に克服しつつあるが、それでもなお「ストリームの乱立によるリソース枯渇」という根本的な脅威が消えたわけではない。

プロトコルスペシャリストとして、我々はパケットの息づかいを感じ取り、カーネルのメモリ割り当てからアプリケーションの挙動までを一本の線で捉える俯瞰的な視点を持ち続けなければならない。それこそが、真にレジリエントなインフラストラクチャを構築するための唯一にして最大の武器なのだ。

コメント

タイトルとURLをコピーしました