HTTP/2の裏側で何が起きているのか?「偽のヘッダー攻撃」とHPACKの暗部を暴く
おい、ちょっとこっちに来てくれ。
先日、うちのチームが運用しているAPIゲートウェイの前段で、ちょっと奇妙なアラートが検知されたんだ。CPU使用率が突然跳ね上がり、特定のコネクションだけが極端にメモリを消費してフリーズしかけるというね。犯人を突き詰めていくと、原因はHTTP/2の「ヘッダー圧縮(HPACK)」と、それにまつわる巧妙な不正リクエスト、いわゆる「偽のヘッダー攻撃(Pseudo-Header Injection / HPACK Bomb)」だった。
HTTP/2は、HTTP/1.xの限界だったHead-of-Line阻塞(行頭ブロック)を打破し、1本のTCPコネクション上で複数のリクエストを同時に流す「マルチプレクシング」を手に入れた。爆発的な高速化の裏で、プロトコル構造が複雑化したことは言うまでもない。
今日は、シニアである俺が、現場のトラブルシューティングの視点を交えながら、HTTP/2のヘッダー構造の裏側と、そこに潜むセキュリティの罠、そしてインフラエンジニアとしてどう身を守るべきかを徹底的に叩き込んでやる。
—
1. HTTP/2ヘッダーの基礎:仮想空間を飛ぶ「偽のヘッダー(Pseudo-Headers)」
まずは基本のおさらいだ。HTTP/1.xでは、メソッド(`GET`など)やパス(`/index.html`)、ホスト名などはすべてプレーンテキストのHTTPヘッダーとして行ごとに流れていた。しかし、HTTP/2ではバイナリフレーミング層が導入され、これらはすべて「ヘッダーブロック」としてエンコードされる。
ここで登場するのが、HTTP/2特有の「疑似ヘッダーフィールド(Pseudo-Header Fields)」だ。
疑似ヘッダーは、HTTP/1.xのリクエスト行にあった情報を構造化したもので、必ずコロン(`:`)から始まる。
- `:method`: リクエストメソッド(例: `GET`, `POST`)
- `:scheme`: スキーム(例: `https`)
- `:authority`: ホスト名とポート番号(HTTP/1.xの `Host` ヘッダーに相当)
- `:path`: リクエストターゲットのパスとクエリ文字列
これらはRFC 7540(およびHTTP/2の仕様)において、「通常のヘッダーよりも必ず先に配置しなければならない」という厳格なルールがある。もし、通常のヘッダーの後に疑似ヘッダーが送り込まれてきたり、存在しない疑似ヘッダーが混入していたりした場合、それはプロトコル違反――すなわち「偽のヘッダー攻撃」の足音が聞こえているサインだ。
—
2. 恐怖のメカニズム:なぜHPACKは狙われるのか?
HTTP/2の高速化を支える最大の功労者が、ヘッダー圧縮アルゴリズム「HPACK(RFC 7540 / RFC 7541)」だ。
毎回同じようなヘッダー(`User-Agent`や`Accept-Encoding`など)をそのまま流すのは無駄だということで、HPACKは送信側と受信側で「静的テーブル(Static Table)」と「動的テーブル(Dynamic Table)」という辞書を共有し、インデックス番号だけでヘッダーをやり取りする。
ここに、悪意ある攻撃者がつけ入る隙がある。
2.1 HPACKボム(Compression Bomb)
攻撃者は、非常に小さな圧縮データ(数バイト)でありながら、展開すると数メガバイト、あるいは数ギガバイトにも膨れ上がるような悪意あるHPACKのハフマン符号や動的テーブル操作コマンドを構築し、サーバーに送りつける。
サーバーは律儀にこれをデコードし、メモリ上に展開(テーブルを肥大化)させようとするため、あっという間にメモリが枯渇し、OOM Killer(Out of Memory Killer)の餌食になる。これがHPACKを狙った代表的なサービス停止(DoS)攻撃だ。
2.2 不正な疑似ヘッダーインジェクション
HTTP/2のストリーム上で、サーバーのルーティングロジックやリバースプロキシを誤認させるために、存在しない疑似ヘッダーや、矛盾した`:method`と`:path`の組み合わせを送りつける手口だ。
例えば、CDNやリバースプロキシ(NginxやEnvoyなど)が適切にバリデーションを行っていない場合、バックエンドのアプリケーションに予期せぬパース結果が渡り、セキュリティ機構をバイパスしてしまう脆弱性(Request Smugglingの温床)に繋がる。
—
3. 通信の裏側:パケットとシーケンスのリアル
実際に、攻撃者がどのように不正なヘッダーを流し込み、サーバーがどう防衛するのか、その通信フローを追ってみよう。
[攻撃者 / クライアント] [リバースプロキシ / サーバー]
│ │
│── 1. HEADERSフレーム(不正な疑似ヘッダー) ───>│
│ (:method = POST, :path = /admin, │
│ 存在しない偽ヘッダー: X-Evil-Inject) │
│ │
│ 【HPACKデコード処理】
│ ・テーブルサイズの限界チェック
│ ・疑似ヘッダーの順序・重複検証
│ │
│<── 2. RST_STREAM (ERROR_CODE: PROTOCOL_ERROR) ──│
│ (不正を検知し、即座にストリームを強制切断) │
│ │
現場のデバッグで`tcpdump`やWiresharkを開いたとき、HTTP/2のコネクション上で突然 `RST_STREAM` が飛んできたら、大体はこのレイヤーで何らかのプロトコル違反やバリデーションエラーが起きた証拠だ。
---
4. 実務で使える!サーバー・プロキシ側の防御設定とコード例
じゃあ、俺たちはインフラエンジニア、バックエンドエンジニアとしてどう防衛すればいいのか。口で言うだけじゃなく、具体的な設定とコードを見せていこう。
4.1 Nginxでの設定とパラメータチューニング
Nginxをフロントに立てる場合、HTTP/2のHPACKに関するバッファやテーブルサイズを適切に制限し、過大なメモリ消費を防ぐ必要がある。`nginx.conf`の最適解を見てくれ。
http {
# HTTP/2接続時の設定
# 動的テーブルの最大サイズを制限し、HPACKボムによるメモリ枯渇を防ぐ
http2_max_field_size 4k; # 個々のヘッダーフィールドの最大長
http2_max_header_size 16k; # リクエストヘッダー全体の最大長
# 同時ストリーム数の制限(リソース保護)
http2_max_concurrent_streams 128;
server {
listen 443 ssl http2;
server_name api.example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://backend_cluster;
# プロキシ転送時にも不正なヘッダーや疑似ヘッダーの混入を防ぐ
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 不正な文字が含まれるヘッダーを弾く(NGINX Plusやモジュールによる厳格化)
# 基本的にリバースプロキシ側でRFC 7540に厳密なリクエストのみを通す
}
}
}
4.2 Python (FastAPI / Uvicorn) でのAPI設計と検証
もしあなたがPythonでモダンなWeb APIを組んでいるなら、ASGIサーバー(Uvicornなど)がデフォルトで一定のプロトコル違反を防いでくれるが、アプリケーション層でも不正な入力値に対する警戒怠るな。
以下は、`curl`や不正なクライアントから送られてきたリクエストの挙動をテストするための、Python(HTTPX)を用いたクライアント側のテストコードだ。
test_http2_client.py
import httpx
import asyncio
async def test_malformed_request():
# 注意: 通常のHTTPXクライアントは不正な疑似ヘッダーを自動でガードしますが、
# 低レイヤーの挙動を模したリクエストテストの概念コードです。
url = “https://api.example.com/v1/data”
headers = {
# 正常なヘッダーの中に意図しない特殊文字や制御文字を混ぜるテスト
“X-Custom-Header”: “InjectedValue\r\nTransfer-Encoding: chunked”
}
print(“HTTP/2リクエストを送信中…”)
async with httpx.AsyncClient(http2=True) as client:
try:
# サーバー側がHPACKや疑似ヘッダーの不正を検知した場合、
# httpx.RemoteProtocolError や httpx.LocalProtocolError が発生します。
response = await client.get(url, headers=headers)
print(f”レスポンスステータス: {response.status_code}”)
print(response.text)
except httpx.ProtocolError as e:
print(f”[防御成功] プロトコルエラーを検知しました: {e}”)
if __name__ == “__main__”:
asyncio.run(test_malformed_request())
4.3 現場でのデバッグ:curlコマンドによるHTTP/2通信のぞき見
トラブルシューティングの現場では、まず手元の `curl` でHTTP/2のネゴシエーションやヘッダーの返り値を確認するのが定石だ。
HTTP/2が正しく有効になっているか、詳細な通信ログ(トレース)と共に確認する
curl -iv –http2 https://api.example.com/health
もし、サーバー側で脆弱性対策のパッチが当たっていない、あるいは設定が緩い場合、この `curl` を使ってカスタムヘッダーにコロン(`:`)を含む文字列や制御文字をインジェクションし、プロキシがどう反応するか(502 Bad GatewayやRST_STREAMになるか)をテストすることができる。
—
5. シニアからの教訓:これからのWebインフラを守るために
HTTP/2(そして現在普及が進むHTTP/3)は、ウェブを圧倒的に速くした。しかし、「速さの裏には常に新たな複雑性と攻撃surface(攻撃対象領域)が潜んでいる」という事実を、俺たちは忘れてはならない。
実務でAPIゲートウェイやロードバランサー、アプリケーションサーバーを選定・設定する際は、以下の3点を必ず死守してほしい。
1. プロトコルスタックのアップデートを怠らない
Nginx, Envoy, Goの `net/http` など、HTTP/2のHPACK実装に脆弱性(CVE-2023-44487など、いわゆる「HTTP/2 Rapid Reset攻撃」やHPACK関連の脆弱性)が発見された際、パッチの適用スピードが生死を分ける。ミドルウェアのバージョン管理を甘く見てはいけない。
2. リバースプロキシでの厳格なバリデーション
バックエンドのアプリケーションサーバーに直接生のHTTP/2トラフィックを曝してはいけない。必ず堅牢なリバースプロキシ(EnvoyやNginxなど)を挟み、そこで疑似ヘッダーの順序、重複、不正な文字コードをフィルタリングさせろ。
3. リソース制限(クォータ)の徹底
コネクションあたりの最大ストリーム数、ヘッダーの最大サイズ、HPACKの動的テーブルサイズを適切に制限し、リソース枯渇型のDoS攻撃(HPACKボム)を物理的に無力化する設定を入れ忘れないこと。
ネットワークの裏側でパケットがどう踊り、どう処理されているか。その解像度を上げることが、トラブルを最短で解決し、強固なインフラを作り上げる唯一の道だ。
さて、コーヒーブレイクは終わりだ。さっそく本番環境の設定ファイルのレビューに戻るとしようか。
コメント