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

HTTP/2マルチプレクシングの裏側:ストリーム多重化攻撃と、現場のエンジニアが守るべきリソース防衛術

こんにちは。ネットワークの配管からアプリケーション層のパケットの息遣いまで、数々の修羅場をくぐり抜けてきたインフラエンジニアの私だ。

Webの高速化、そしてレイテンシの極限までの削減を旗印に普及した「HTTP/2」。TCPコネクションを1本張るだけで、その中で複数のリクエストとレスポンスを同時並行(マルチプレクシング)で流せるようになったあの革命的な感動を、君も覚えているだろうか?

しかし、光が強ければ影も濃くなる。HTTP/2の最大の武器である「ストリームの多重化」は、設計を誤れば、サーバーを瞬時に沈黙させる凶悪なアキレス腱に変貌する。

今回は、実務でWeb APIやインフラの設計・運用に携わる君に向けて、HTTP/2の心臓部であるストリームの仕組み、それがどのように牙をむくのかという脆弱性のメカニズム、そして現場のエンジニアとしてどう防衛すべきかを、泥臭い実例を交えながら徹底的に解説していこう。

—

1. HTTP/2の基礎:1本のTCP上で躍動する「ストリーム」の概念

HTTP/1.1の時代、私たちは「Head-of-Line Blocking(行頭ブロック)」という呪いに苦しめられていた。1つのTCPコネクション上で前のリクエストのレスポンスが終わるまで、次のリクエストが待たされる。これを回避するためにブラウザは6本ものTCPコネクションを張り、OSのソケット枯渇やスリーウェイハンドシェイクのオーバーヘッドに泣かされていたのだ。

HTTP/2はこの問題を、「1本のTCPコネクション上に仮想的なレーン(ストリーム)を何本も敷く」ことで解決した。

[ TCP Connection ]
├─ Stream ID: 1 (GET /index.html) ──> 同時並行で
├─ Stream ID: 3 (GET /api/v1/user) ──> 流れる
└─ Stream ID: 5 (GET /style.css) ──> バイナリフレーム

すべてのHTTPメッセージ(ヘッダーやボディ)は、「フレーム(Frame)」という小さなバイナリの単位に分割され、それぞれに「ストリームID(Stream ID)」が付与される。サーバー側は、異なるストリームIDのフレームをインターリーブ(交互に混ざった状態)で受け取り、内部で器用に組み立て直して処理する。

この「細かく刻んで同時に流す」という仕組みこそが、HTTP/2を爆速たらしめている正体だ。だが同時に、この「何本でもストリームを作れてしまう」という自由度が、悪意ある攻撃者にとって格好の武器になってしまう。

—

2. 脅威の正体:ストリーム多重化攻撃(Stream Multiplexing Attack)

HTTP/2の仕様では、クライアントは1つのTCPコネクション上で、同時に何百、何千ものストリームを開くことができる。ここで考えてみてほしい。

もし、悪意あるクライアントが「重い処理を要求するリクエスト」や「無限に完了しないリクエスト」を、数千〜数万のストリームを同時にオープンして送りつけたらどうなるか?

これが、ストリーム多重化攻撃(Stream Multiplexing Attack)、あるいは派生形である「RST_STREAMフラッド攻撃」の全貌だ。

サーバー内部で何が起きているのか?

1. メモリの圧迫: サーバー(Nginx、Envoy、あるいは独自のWeb APIサーバー)は、開かれたストリームごとに状態管理用のメモリ(バッファやコンテキスト)を割り当てる。ストリーム数が数万に達すれば、数GBのメモリが瞬時に溶ける。
2. CPUリソースの枯渇: バックエンドのデータベースやアプリケーションサーバーへリクエストが雪崩を打って到達し、スレッドプールやコネクションプールが完全に枯渇する。
3. 正当なユーザーの締め出し(DoS): リソースを食いつぶされたサーバーは、他の正当なユーザーからのリクエストに応答できなくなる。

厄介なのは、これらがたった1本のTCPコネクション、たった1台のクライアント端末から実行可能だという点だ。従来のL4層のDDoS対策(IP単位の接続数制限など)をいとも簡単にすり抜けてしまう。

—

3. 現場で使える防衛線:SETTINGS_MAX_CONCURRENT_STREAMSの掌握

この攻撃に対する最も基本的かつ強力な盾が、HTTP/2の仕様(RFC 7540)に組み込まれている `SETTINGS_MAX_CONCURRENT_STREAMS` パラメーターだ。

パラメーターの意味

この設定値は、「1つのTCPコネクション上で、同時にアクティブにできるストリームの最大数」をサーバー側からクライアントに通知するものである。

  • デフォルト: RFC上では特に厳しい制限は強制されていない(事実上の無制限、あるいは実装依存で数千など)。
  • 推奨値(実務的): 一般的なWebアプリケーションであれば、`100` から `250` 程度に絞るのがセオリーだ。ブラウザからのアクセスであれば、通常この数があれば十分に高速かつ安全に動作する。

実務における設定例(Nginx)

現場のインフラを守るため、Nginxのリバースプロキシ層でこの値を厳格に制限しよう。`nginx.conf` の設定例を挙げる。

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

# SSL/TLSの設定は省略…

# 1つのコネクションで同時に処理するストリーム数を「128」に制限
# これにより、単一クライアントからのリソース枯渇攻撃を防ぐ
http2_max_concurrent_streams 128;

# ついでに、1つのコネクションで処理できる最大リクエスト数も制限しておくと吉
keepalive_requests 1000;

location / {
proxy_pass http://backend_cluster;
# バックエンドへのタイムアウト設定も必ずセットで行うこと
proxy_read_timeout 60s;
}
}
}

もしクライアント(あるいは攻撃者)がこの制限値(128)を超えるストリームを開こうとした場合、サーバー側は `RST_STREAM` フレーム(エラーコード: `REFUSED_STREAM`)を返して拒絶する。サーバーのメモリとCPUは守られるというわけだ。

—

4. 実験と検証:curlとPythonで見るストリーム制御の挙動

百聞は一見に如かず。実際にHTTP/2のストリームがどのように扱われているか、手元の環境で検証してみよう。

① `curl` によるHTTP/2のバージョン確認

まずは対象のサーバーが正しくHTTP/2で応答しており、多重化の恩恵を受けているかを `curl` で確認する。

–http2 オプションを指定してリクエストを送信
詳細な通信ログ (-v) を出力し、HTTP/2でネゴシエーションされているか確認する
curl -v –http2 https://api.example.com/health

レスポンスヘッダーに `< HTTP/2 200` や、ALPN(Application-Layer Protocol Negotiation)で `h2` が選択されたログが見つかれば成功だ。

② Python (httpx) による並行リクエストのシミュレーション

現代のWeb API開発において、クライアント側がHTTP/2のマルチプレクシングを活用して効率的にリクエストを投げるコードを書いてみよう。Pythonの `httpx` ライブラリは標準でHTTP/2をサポートしている。

import asyncio
import httpx

async def fetch_api(client: httpx.AsyncClient, url: str, stream_id: int):
try:
# 1つのHTTP/2セッション(コネクション)を共有しながら非同期でリクエストを投げる
response = await client.get(url)
print(f”[Stream {stream_id}] Status: {response.status_code}, Body: {response.text[:30]}”)
except Exception as e:
print(f”[Stream {stream_id}] Error: {e}”)

async def main():
url = “https://api.example.com/data”

# http2=True を有効化してクライアントを初期化
async with httpx.AsyncClient(http2=True) as client:
# 同時に50個のリクエストを非同期で送信(1本のTCPコネクション上で多重化される)
tasks = [fetch_api(client, url, i) for i in range(50)]
await asyncio.gather(tasks)

if __name__ == “__main__”:
# イベントループの実行
asyncio.run(main())

このスクリプトを実行すると、50個のリクエストがTCPの新規ハンドシェイクなしに、一瞬でサーバーへ飛び込む。サーバー側の `http2_max_concurrent_streams` が例えば `30` に設定されている場合、後半のストリームはサーバーからリジェクトされるか、キューイングされて順番に処理されることになる。

—

5. シニアネットワークエンジニアからの実務的アドバイスと教訓

最後に、長年インフラの現場に立ち会い、こうしたプロトコル起因の障害を幾度となく踏んできた私から、君への実践的なTipsをいくつか授けよう。

1. デフォルト値を信じるな
Nginx、Envoy、Apache、あるいはNode.jsやGoの標準HTTP/2サーバーライブラリ。すべてのデフォルト設定がセキュアとは限らない。特にクラウドネイティブな環境でEnvoyやAPI Gatewayを使う場合、`max_concurrent_streams` や各種タイムアウト値(`stream_idle_timeout` など)が適切にチューニングされているか、必ずコードレビューの項目に入れなさい。
2. WAF(Web Application Firewall)やL7ロードバランサーでの多重防御
リバースポキシの手前にAWS ALBやCloudflareなどのクラウド型WAF/LBを挟んでいる場合でも油断してはいけない。バックエンドのアプリケーションサーバーが直接インターネットに露出している構成であれば、そこが致命的な脆弱性になる。境界防御だけでなく、ゼロトラストの思想をもって各レイヤーで制限をかけること。
3. メトリクス監視の徹底
PrometheusやGrafanaで、現在オープンされているアクティブなストリーム数や、`RST_STREAM` の発生レートを必ずモニタリングしておこう。攻撃を受けている時は、トラフィックの容量(Gbps)ではなく、「コネクションあたりのストリーム数の異常な高騰」や「特定のクライアントIPからの切断頻発」としてシグナルが現れる。

HTTP/2は素晴らしい技術だ。しかし、プロトコルの深い仕様を知らずに「速いらしいから」と導入するのは、扱い方を知らない強力な銃を子供に持たせるようなものだ。

仕組みを理解し、適切なパラメータで武装し、安全かつ爆速なネットワークインフラを君の手で構築してほしい健闘を祈る。

コメント

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