【実務・中級編】HTTP/2のセキュリティ脆弱性:ストリーム多重化攻撃 – HTTPプロトコル・通信規格実践ガイド

HTTP/2ストリーム多重化攻撃の罠:1本のTCPコネクションがサーバーを沈める理由

「HTTP/2を有効にしたら、WebサイトやAPIのレスポンスが劇的に改善した!」
――そんな歓喜の声がインフラエンジニアの間で囁かれて久しい。複数のTCPコネクションを張る必要がなくなり、1本のコネクション上で複数のリクエストを同時に流し込める「マルチプレクシング(多重化)」。この美しい技術に魅せられ、モダンなWebの恩恵をフルに受けているシステムは多いはずだ。

だが、少し待ってほしい。
その「1本のコネクションで何でもできる」という特性が、裏を返せば最悪の凶器になり得ることを、君は意識したことがあるだろうか?

今日は、数々の修羅場をくぐり抜けてきたシニアネットワークエンジニアの私から、HTTP/2の裏側に潜む最大のダークサイド――「ストリーム多重化攻撃(Stream Multiplexing Attack / Rapid Reset等)」のメカニズムと、それを実務でどう防ぐべきかについて、現場のリアルな知見を交えて徹底的に解説しよう。

—

1. なぜマルチプレクシングは攻撃者に狙われるのか?

HTTP/1.1の時代、サーバーに大量のリクエストを叩き込むDDoS攻撃といえば、TCPの3ウェイハンドシェイクを悪用したSYNフラッドや、大量のHTTP GETリクエストでプロセスを飽和させるアプローチが主流だった。これらは、ロードバランサーやWAF(Web Application Firewall)、あるいはOSのコネクション制限(`somaxconn`など)で、ある程度は物理的・論理的にいなすことができた。

しかし、HTTP/2の登場によりゲームのルールが変わった。

HTTP/2では、1つのTCPコネクションの内部を「ストリーム(Stream)」という仮想的な通信レーンに分割し、その上でフレーム(HEADERSやDATAなど)をインタリーブ(混在)させてやり取りする。

[ TCP コネクション (1本) ]
├── Stream 1 (HEADERS / DATA) ──> リクエストA
├── Stream 3 (HEADERS / DATA) ──> リクエストB
└── Stream 5 (RST_STREAM) ──> 攻撃:即座にリセット!

ここで攻撃者が何を企むか。
彼らは1本のTCPコネクションを確立した後、数千、数万ものストリームを同時にオープンし、サーバーに処理を要求する。サーバー側は、それぞれのストリームに対してメモリを割り当て、ヘッダーをデコードし、リクエストコンテキストを生成しなければならない。

結果はどうなるか?
TCPコネクション数は「たった1本」であるため、一般的なL4ファイアウォールやコネクション数ベースのレートリミッターは「あ、健全な単一のクライアントだな」と勘違いしてスルーしてしまう。その裏で、アプリケーションサーバーのメモリとCPUリソースが音を立てて枯渇し、正当なユーザーのリクエストまでが一切通らなくなる――これがストリーム多重化攻撃の恐るべき実態だ。

—

2. 通信の裏側:何が起きているのか?(シーケンスとフレームの挙動)

実務でこの脅威に対処するためには、パケットレベルで何が起きているのかを把握しておく必要がある。正常な通信と、攻撃的な通信のシーケンスを比較してみよう。

正常なHTTP/2マルチプレクシングのフロー

Client Server
| —– [接続確立: TCP Handshake + TLS Handshake] ——-> |
| —– [SETTINGSフレーム交換] ————————-> |
| |
| — HEADERS (Stream ID: 1) —————————–> | (処理開始)
| — HEADERS (Stream ID: 3) —————————–> | (処理開始)
| |
| <--- DATA / HEADERS (Stream ID: 1) --------------------- | (応答返却) | <--- DATA / HEADERS (Stream ID: 3) --------------------- | (応答返却)

攻撃時(Rapid Reset等)のフロー

悪名高い「CVE-2023-44487(HTTP/2 Rapid Reset攻撃)」などが典型例だが、攻撃者は次のような荒業を使う。

Client Server
| —– [TCP/TLSコネクション確立] ———————–> |
| |
| — HEADERS (Stream ID: 1) —————————–> | (サーバーがメモリ割当)
| — RST_STREAM (Stream ID: 1, Error: Cancel) ———–> | (即座にキャンセル)
| — HEADERS (Stream ID: 3) —————————–> | (サーバーがメモリ割当)
| — RST_STREAM (Stream ID: 3, Error: Cancel) ———–> | (即座にキャンセル)
(これを数万回高速に繰り返す!)

クライアントは「リクエストを送るやいなや、即座に`RST_STREAM`(リセット)」を送信する。クライアント側にとってはCPU負荷がほとんどない。しかし、サーバー側は「リクエストを受け取ってストリーム構造体を作り、その直後に破棄する」という重い処理を強制される。この非対称性が、サーバーを一瞬でクラッシュさせる原因となる。

—

3. 鍵を握るパラメーター:「SETTINGSフレーム」の制御

HTTP/2の仕様(RFC 7540)では、こうしたリソース枯渇を防ぐための防衛メカニズムがちゃんと用意されている。それが`SETTINGS`フレームだ。

コネクション確立直後、クライアントとサーバー双方がこのフレームを交換し、お互いの通信パラメータ(ノブ)を合意する。インフラエンジニアとして絶対に押さえておくべき主要なパラメータは以下の通り。

1. `SETTINGS_MAX_CONCURRENT_STREAMS` (ID: 0x3)

  • 意味: 1本のTCPコネクション上で、同時にアクティブにできる最大ストリーム数。
  • なぜ重要か: これが無制限(あるいはデフォルトで巨大な値)になっていると、1人の悪意あるクライアントに無数のストリームを占有される。実務では、この値を適切に制限(例: 100〜250程度)することが鉄則だ。

2. `SETTINGS_INITIAL_WINDOW_SIZE` (ID: 0x4)

  • 意味: フロー制御におけるストリームごとの初期ウィンドウサイズ(バイト単位)。
  • なぜ重要か: サーバーが一度に受け入れるデータ量を制限し、メモリバッファの暴騰を防ぐ。

—

4. 実務での防衛策:Nginx / Envoy / Web APIの実装設定

では、現場のインフラやコードでどうこれらを迎え撃つか。具体的な設定ファイルのサンプルを見ていこう。

ケースA: NginxでのHTTP/2リソース制限設定

Nginxは、HTTP/2モジュールを通じてストリーム数やバッファを厳格にコントロールできる。`nginx.conf`のサーバーブロックやhttpブロックには、以下のような防御的設定を施そう。

http {
# HTTP/2接続全体のバッファやリクエスト制限を最適化

server {
listen 443 ssl http2;
server_name api.example.com;

ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;

# 同時ストリーム数の上限を厳しく絞る(デフォルトは128だが、API用途ならさらに精査)
# ※ nginxのバージョンやモジュールによりディレクティブが異なる場合があります
http2_max_concurrent_streams 128;

# クライアントが一度に送信できるリクエストボディのバッファサイズ制限
client_body_buffer_size 16k;
client_max_body_size 1m;

location / {
# レートリミット(zone定義が別途必要)を適用し、異常なリクエスト頻度を弾く
limit_req zone=api_limit burst=20 nodelay;

proxy_pass http://backend_cluster;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
}

ケースB: Envoy Proxy(マイクロサービス・メッシュ)での設定

モダンなクラウドネイティブアーキテクチャでは、EnvoyがAPI Gatewayやサイドカーとして最前線に立つことが多い。Envoyでは、`http2_protocol_options`でストリームのライフサイクルを細かく制御できる。

Envoyのリスナー設定スニペット
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
codec_type: HTTP2
http2_protocol_options:
# 同時ストリーム数の最大値を100に厳格制限
max_concurrent_streams: 100
# 1つのコネクションあたりの最大リクエスト数を制限(長期接続でのローテーション用)
max_requests_per_connection: 10000
# 初期ウィンドウサイズを小さめに設定してメモリを守る
initial_stream_window_size: 65536 # 64KB
initial_connection_window_size: 1048576 # 1MB

—

5. デバッグと検証:自社システムは耐えられるか?

「うちのAPIサーバー、本当にHTTP/2攻撃に耐えられるのか?」
それを確かめるには、ステージング環境において意図的に負荷をかけてみる(ペネトレーションテストや負荷テストを行う)のが一番の近道だ。

現場でよく使われる検証用スクリプトやツール(Go言語製のものや、カスタムスクリプト)の概念を理解しておこう。ここでは、Pythonの `h2` ライブラリ(HTTP/2低レベル実装)を使って、意図的に大量のストリームをオープンする挙動をシミュレートするイメージをコードで示す。

> ⚠️ 警告: 以下のコードはHTTP/2の挙動理解のための教育目的です。許可されていない外部サーバーに対して実行すると不正アクセス禁止法や業務妨害に問われます。必ず検証用のローカル環境(`localhost`等)でのみ実行してください。

【教育・検証用】HTTP/2ストリーム多重化の挙動確認スクリプト
import socket
import h2.connection
import h2.config

def simulate_stream_flood(host, port, num_streams=500):
“””
指定したサーバーに対して大量のHTTP/2ストリームを同時にオープンし、
RST_STREAMを送りつけることでサーバーの応答と負荷を確認するサンプル。
“””
# 1. 普通のTCPソケットで接続
sock = socket.create_connection((host, port))

# 2. HTTP/2のクライアント接続設定を初期化
config = h2.config.H2Configuration(client_side=True)
conn = h2.connection.H2Connection(config=config)
conn.initiate_connection()
sock.sendall(conn.data_to_send())

print(f”[] {host}:{port} へ接続完了。{num_streams}個のストリームをオープンします…”)

try:
# 3. 大量のストリームを連続して生成(HEADERSフレーム送信)
for i in range(1, num_streams + 1):
stream_id = conn.get_next_available_stream_id()
headers = [
(‘:method’, ‘GET’),
(‘:path’, ‘/’),
(‘:authority’, host),
(‘:scheme’, ‘https’),
]
# ヘッダーを送信(これでサーバー側にストリームが作られる)
conn.send_headers(stream_id, headers, end_stream=True)

# あえてすぐにリセット(RST_STREAM)を送りつける場合:
# conn.reset_stream(stream_id, error_code=h2.errors.ErrorCodes.CANCEL)

sock.sendall(conn.data_to_send())
print(“[+] ストリームの送信が完了しました。サーバーの挙動を確認してください。”)

except Exception as e:
print(f”[-] エラーが発生しました(サーバーが遮断した可能性あり): {e}”)
finally:
sock.close()

if __name__ == “__main__”:
# ※必ずローカルの安全なテスト環境に対して実行すること
# simulate_stream_flood(“127.0.0.1”, 443, num_streams=1000)
pass

もし、このスクリプトを実行した瞬間にサーバーのCPU使用率が100%に張り付いたり、プロセスがOOM Killer(Out Of Memory)に狩られたりするようであれば、君のインフラはストリーム多重化攻撃に対して無防備だと言わざるを得ない。今すぐ、前述した `max_concurrent_streams` の設定を見直すべきだ。

—

シニアエンジニアからの実務的アドバイス

HTTP/2は素晴らしいプロトコルだが、「便利=安全」ではない。マルチプレクシングという強力な仕組みの裏側には、常にリソース枯渇というリスクが隣り合わせで存在している。

実務でAPI基盤やWebフロントエンドのインフラを設計・運用する際は、以下のチェックリストを必ず頭に叩き込んでおいてほしい。

1. デフォルトを信じるな: NginxやEnvoy、あるいはGo/Node.js等のアプリケーションフレームワークのデフォルト設定は、必ずしもセキュリティ的に硬化(Hardening)されていない。
2. `SETTINGS_MAX_CONCURRENT_STREAMS`を適切に絞る: 無制限にする理由はゼロだ。サービスの規模やクライアントの特性に合わせ、100〜200前後の適切なスレッド・ストリーム数に制限しよう。
3. WAFやレイートリミットとの連携: TCPコネクション数だけでなく、HTTP/2のストリーム発生頻度そのものを監視・制限できる次世代型のWAFやプロキシの導入を検討する。
4. 定期的な脆弱性情報のキャッチアップ: CVE-2023-44487(Rapid Reset)のような新しい攻撃手法は突如としてやってくる。利用しているミドルウェア(Nginx, Apache, Envoy, 各種CDN)のセキュリティアドバイザリを常に注視し、速やかにパッチを適用できる体制を整えておくこと。

ネットワークの進化の歴史は、そのまま「攻撃者とのイタチごっこ」の歴史でもある。プロトコルの隅々まで理解し、コントロールを握っているのは我々エンジニアだ。堅牢で美しいインフラを守り抜くためにも、今日の学びをさっそく明日の設定レビューに活かしてほしい。

コメント

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