【実務・中級編】HTTP/2実装におけるメモリ枯渇攻撃(DoS)の防御 – HTTPプロトコル・通信規格実践ガイド

HTTP/2の美徳を悪用する悪意:マルチプレクシングの裏に潜むメモリ枯渇攻撃と、現場のプロが仕込む「防壁」

ネットワークエンジニアなら誰もが知っている通り、HTTP/2はWebのスピードを劇的に変えた立役者だ。1つのTCPコネクション上で複数のリクエストとレスポンスを同時に多重化(マルチプレクシング)し、HTTP/1.xの足かせであった「Head-of-Line Blocking(HOLブロック)」を華麗に打ち破った。

しかし、光が強ければ影も濃くなる。

この「1つのコネクションで何でも同時に並行処理できる」というHTTP/2の圧倒的な利便性は、裏を返せば、悪意ある攻撃者にとって「サーバーのメモリを効率よく食い潰すための格好の踏み台」になり得る。
今回は、実務でWeb APIやインフラの設計・運用に携わる君たちに向けて、HTTP/2の心臓部であるストリームとHPACKが抱える脆弱性の正体と、それを実戦でどう封じ込めるのか、泥臭い設定値やコードを交えて徹底的に解説しよう。

—

1. なぜHTTP/2は「メモリ枯渇(DoS)」に狙われやすいのか?

HTTP/1.xの時代、サーバーを守るのは比較的シンプルだった。同時接続数(`MaxClients`や`worker_connections`)を絞り、タイムアウトを適切に設定しておけば、行儀の悪いクライアントは容易に切り捨てられた。

だが、HTTP/2の世界ではゲームのルールが違う。

攻撃者は1本のTCPコネクションを確立し、その中で数百、数千という「仮想的な通信路(ストリーム)」を爆発的に生成する。TCPのハンドシェイクコストはたった1回分。サーバー側は、それぞれのストリームの状態を維持するためにメモリ(ストリームバッファや状態管理用の構造体)を割り当てざるを得ない。

ここに、HTTP/2特有の2大脅威が存在する。

1. ストリーム洪水(Stream Multiplexing Abuse):
数千のストリームを同時にオープンし、サーバーに処理の継続を強いることで、メモリとCPUリソースを枯渇させる。
2. HPACKによるヘッダー圧縮爆弾(HPACK Bomb):
HPACKの「動的テーブル(Dynamic Table)」の仕組みを悪用する。膨大な数の巨大な(あるいはハフマン符号化された)ヘッダーを送りつけることで、サーバー側のデコード用メモリをパンクさせる。

これらはRFC 7540(HTTP/2仕様)の策定段階から危惧されており、サーバー実装者やインフラエンジニアが「明示的なリミット」を設けない限り、いとも簡単にサービス停止(Out of Memory / OOM)へと追い込まれる。

—

2. パケットの裏側で何が起きているか?(通信フローと設定の攻防)

まずは、HTTP/2のコネクション確立直後に交わされる「SETTINGSフレーム」のやり取りを見てみよう。ここが防御の最前線だ。

[クライアント] [HTTP/2サーバー (Nginx / Envoy等)]
| |
| — 1. TCP 3-way Handshake ———————-> |
| — 2. Connection Preface (PRI HTTP/2…) —-> |
| — 3. SETTINGS (初期パラメータ通知) ————–> |
| <--- 4. SETTINGS (サーバー側の制限値を通知) ------- | | - SETTINGS_MAX_CONCURRENT_STREAMS: 100 | | - SETTINGS_HEADER_TABLE_SIZE: 4096 | | | | --- 5. HEADERS / DATA (ストリーム大量生成) ------> |
| (ここで制限値を超えると RST_STREAM で即座に切断)

このシーケンスの「3」と「4」で交換される`SETTINGS`フレームこそが、サーバー側の身を守るための防壁パラメータである。現場のエンジニアとして必ず押さえておくべき主要なパラメータは以下の3つだ。

  • `SETTINGS_MAX_CONCURRENT_STREAMS`:

1つのコネクション上で同時に処理を許可するストリームの最大数。これを無制限にしてはいけない。

  • `SETTINGS_HEADER_TABLE_SIZE`:

HPACKの動的テーブルの最大サイズ。攻撃者はこれを大きくしようとするが、サーバー側で厳しく制限(通常4KB〜64KB程度)すべきだ。

  • `SETTINGS_INITIAL_WINDOW_SIZE`:

フロー制御のウィンドウサイズ。これが大きすぎると、巨大なペイロードを一気に送り込まれてメモリが溢れる。

—

3. 現場で即効性のある防御設定(Nginx / Envoy / Goの具体例)

では、実務のインフラ環境において、具体的にどのような設定を施すべきか。主要なプロダクトの設定例を見ていこう。

A. Nginxの場合

Nginxは非常に堅牢なデフォルト値を持っているが、大規模なAPI Gatewayなどでは明示的なチューニングが必要だ。`nginx.conf`の`http`またはセールサーバーブロックに以下を記述する。

http {
# 1つのコネクション内で同時に処理できる最大ストリーム数を絞る(デフォルトは128だが、API用途なら32〜100程度が安全)
http2_max_concurrent_streams 100;

# HPACKデコード用のメモリバッファ上限(大きすぎるとメモリ爆弾の餌食になる)
http2_chunk_size 8k;

# クライアントから送られてくるリクエストボディのバッファサイズ
client_body_buffer_size 16k;
client_max_body_size 1m; # APIの性質に応じて最小限に抑える

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

# その他のSSL/TLS設定…
}
}

B. Envoy Proxyの場合

モダンなマイクロサービスアーキテクチャで多用されるEnvoyでは、`http2_protocol_options`で厳格にリミットをかける。

Envoyのリスナー設定スニペット
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: api_gateway
codec_type: HTTP2
http2_protocol_options:
# 同時ストリーム数の上限を強制
max_concurrent_streams: 100
# 初期流動制御ウィンドウ(デフォルト64KBより大きくしないのが無難)
initial_stream_window_size: 65536
initial_connection_window_size: 1048576 # 1MB
# ヘッダーの最大数やサイズ制限
max_headers_count: 100

—

4. デバッグと検証:本当に防げているか?

「設定を書いたから大丈夫」と過信するのが、一番のシステム障害の引き金になる。ネットワークスペシャリストたるもの、自分で攻撃を模した検証(ペネトレーションテスト)を行い、サーバーが正しくリクエストを弾くか確認するべきだ。

ここでは、Python(`h2`ライブラリ)を用いて、あえて仕様ぎりぎり、あるいは違反するようなストリームを生成する簡易的なテストスクリプトの考え方を示す。

警告: このコードはセキュリティ検証・学習目的のものです。
許可されていない本番環境に対して実行してはいけません。

import h2.connection
import h2.config
import socket
import ssl

def test_http2_dos_mitigation(host, port):
# TLSソケットの確立
ctx = ssl.create_default_context()
ctx.set_alpn_protocols([‘h2’])

sock = socket.create_connection((host, port))
ssl_sock = ctx.wrap_socket(sock, server_hostname=host)

# HTTP/2クライアントとしてのコネクション初期化
config = h2.config.H2Configuration(client_side=True)
conn = h2.connection.H2Connection(config=config)

conn.initiate_connection()
ssl_sock.sendall(conn.data_to_send())

print(f”[] 接続成功: {host}:{port}”)

# 意図的に大量のストリーム(例: 500個)を同時にオープンしてみる
# サーバーの SETTINGS_MAX_CONCURRENT_STREAMS が 100 なら、残りは拒否されるはず
try:
for stream_id in range(1, 1000, 2): # 奇数IDがクライアント起因のストリーム
headers = [
(‘:method’, ‘GET’),
(‘:path’, ‘/health’),
(‘:authority’, host),
(‘:scheme’, ‘https’),
]
conn.send_headers(stream_id, headers)
print(f”[>] ストリーム ID {stream_id} を送信しました”)

ssl_sock.sendall(conn.data_to_send())

except Exception as e:
print([!] サーバー側でブロックまたは切断されました: {e})

finally:
ssl_sock.close()

if __name__ == ‘__main__’:
# ローカルの検証環境に対して実行
# test_http2_dos_mitigation(‘127.0.0.1’, 443)
pass

このスクリプトを実行した際、サーバーのログやメトリクス(Prometheus等)で `RST_STREAM` が返されていること、あるいはTCPコネクション自体が強制切断(`ECONNRESET`)されていることを確認できれば、防壁は正しく機能していると言える。

—

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

最後に、現場でAPIやインフラを構築する君たちに、運用の勘所をいくつか授けておこう。

1. デフォルト値を信じるな
Nginx、Apache、Envoy、あるいはNode.jsやGoの標準HTTP/2サーバー実装であっても、デフォルトのパラメータは「汎用的な互換性」を優先していることが多い。セキュリティと可用性のバランスを取るなら、必ず「自社のAPIが耐えうる限界値」を算出し、厳しめに設定をオーバーライドすること。
2. メトリクスとアラートの監視
HTTP/2特有の異常は、通常のHTTP 5xxエラーとして表に出にくい場合がある。コネクションあたりのアクティブストリーム数、`RST_STREAM`の発生頻度、およびメモリ使用量(RSS)の急激なスパイクを監視グラフに入れておくこと。これこそが、ゼロデイ攻撃やDDoSの早期発見につながる。
3. WAF(Web Application Firewall)の活用
アプリケーションサーバーの手前に、HTTP/2のプロトコル異常を検知・ブロックできるWAFやロードバランサー(AWS ALB, Cloudflare, Cloud Armor等)を配置するのも極めて有効なアプローチだ。インフラの多層防御(ディフェンス・イン・ディフェンス)の原則を忘れないでほしい。

HTTP/2は素晴らしいプロトコルだ。その背後にある複雑なステート管理とメカニズムを正しく理解し、適切なリード(統御)を与えてこそ、真にセキュアで高速なインフラストラクチャが完成する。
日々の運用の中で、パケットの囁きに耳を傾ける余裕を忘れないでいてほしい。

コメント

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