HN 日本語サマリー

← 一覧へ戻る
セキュリティ

QPACKによるHTTP/3サーバーへの増幅攻撃

Qpack amplification attacks against HTTP/3 servers (foxio.io)

15 pointsby kozika1 コメント

要約

HTTP/3サーバーにおけるQPACKヘッダー圧縮の脆弱性が発見されました。認証されていないリモートクライアントが、送信データよりも数千倍のメモリをサーバーに割り当てさせ、サービス拒否(DoS)を引き起こす可能性があります。テストされた実装の7割が影響を受けましたが、多くは既に修正されています。

全文翻訳

ヘッダー・アコーディオン:HTTP/3サーバーに対するQPACK増幅攻撃

TLDR

認証されていないリモートクライアントが、HTTP/3サーバーに送信したデータよりも数千倍多いメモリを割り当てさせることができます。デフォルト設定のサーバーでは、1つの接続でメモリ枯渇(OOM)やフリーズを引き起こす可能性があります。テストされた10個の実装のうち7個が脆弱でしたが、多くは既に修正されています。

QPACKはHTTP/3のヘッダーを圧縮して、通信経路上のトラフィックを最小限に抑えます。影響を受けた実装の多くでは、デコードされたヘッダーのサイズ制限が欠如しているか、不完全であるか、適用が遅すぎます。そのため、小さなHEADERSフレームがメモリ内で数十万ものヘッダーオブジェクトに展開される可能性があります。この展開によるリソース枯渇を防ぐには、フィールドがデコードされ保持される際に累積的な予算を強制する必要があります。

2026年10月20日現在、修正済み

ライブラリ修正バージョン勧告
quic-go0.57.0CVE-2025-64702
Cloudflare quiche0.29.3CVE-2026-12523
lsquic4.9.0CVEなし
Wireshark4.6.2 / 4.4.12CVE-2025-13945

2026年10月20日現在、未修正

ライブラリ
Bun 1.4.3
h3 (hyperium)
aioquic / mitmproxy

緩和策

Bun: HTTP/3サーバーを公開しない
mitmproxy: HTTP/3を無効にする(--set http3=false を渡す)
h3 crate: 適切な max_field_section_size を設定する

影響なし: IIS, Envoy, ngtcp2/nghttp3

背景

当社のアクティブTLSスキャナーであるJA4Scanの様々なQUICスタックに関する最近の研究中に、複数のHTTP/3実装で同じクラスの脆弱性を発見しました。認証されていないリモートクライアントが、エンコードされたリクエストが占めるサイズよりもはるかに多くのメモリをサーバーに割り当てさせるHTTP/3トラフィックを送信できるのです。

この調査は、Gal Bar Nahum氏のHTTP/2リクエストキャンセルDoSに関するMadeYouResetシリーズを読んだ後に始まりました。その研究から、HTTP/3のQPACKヘッダー圧縮に同様の増幅問題があるのではないかと考えました。

HTTP/3はQPACKを使用してヘッダーを圧縮するため、user-agentやcontent-typeのような繰り返し使用されるフィールドは、完全な文字列の代わりに小さなインデックスとして送信できます。QPACKは、仕様で定義された一般的なフィールドのハードコードされたセットである静的テーブルと、クライアントとサーバー間で実行時に新しいヘッダーが表示されるにつれて構築される動的テーブルの2つのルックアップテーブルを使用します。

例えば、strict-transport-security: max-age=31536000; includesubdomains; preloadのような71バイトのヘッダーは、静的テーブルのインデックス58を参照する単一バイト0xFAになります。

RFC 9114セクション4.2.2では、サーバーがデコードされたヘッダーサイズをどのように制限すべきかが定義されています。

フィールドリストのサイズは、フィールドの非圧縮サイズに基づいて計算されます。これには、各フィールドの名前と値のバイト長に加えて、フィールドごとに32バイトのオーバーヘッドが含まれます。

テストされた一部の実装では、これらのうち少なくとも1つが誤っていました。

  1. デコードされたサイズではなく、圧縮されたフレームサイズをチェックする
  2. チェックする前にすべてを解凍する
  3. 32バイトのオーバーヘッドをサイズ計算から除外する
  4. 設定されていない場合にデフォルトで無制限にする
  5. 設定されていても、設定をまったく強制しない

仕組み

目標は、単一のHEADERSフレーム内のヘッダーの数を最大化することです。このバリアントでは、aからz、aaからzz、そして3文字、4文字の名前まで、エンコードされたサイズの予算が満たされるまでユニークな名前を使用して、受信ヘッダーマップに個別のエントリを作成します。2文字の名前は676通りありますが、teヘッダーは特別な値の要件があるためスキップする必要があります。

1,000,000バイトのHEADERSフレームは、この方法で約160,000個のユニークなヘッダーを保持できます。

サーバーがこれらの160,000以上のヘッダーをどのように処理するかが、問題の根源です。

[画像: quic-goでの例示的なリクエストフロー]

アコーディオンは逆方向にも機能します。多くの小さなヘッダーの代わりに、動的テーブルバリアントはエンコーダーストリームを介して1つの大きなエントリ(約4KiB)を挿入し、それを1バイトのインデックス参照で数千回参照します。各参照は、完全なエントリの新しい割り当てとコピーを強制します。これがlsquicとBunの攻撃が機能する仕組みです。各1バイト参照は、4064:1の比率で完全なエントリを具体化します。

実装

実装脆弱性CVEワイヤーからメモリへ根本原因
quic-goはいCVE-2025-64702約15倍のヒープ保持圧縮サイズのみチェック、デコードサイズ制限なし
WiresharkはいCVE-2025-13945クラッシュQPACK dissectorの無制限デコード
Cloudflare quicheはいCVE-2026-12523約20倍32バイトオーバーヘッドの欠落 + フレーム事前割り当て爆弾
lsquicはい修正済み、CVEなし約600-1800倍累積デコードサイズ制限なし、無制限HEADERSブロック
aioquic / mitmproxyはい-約20-30倍MAX_FIELD_SECTION_SIZEは解析されるが、決して強制されない
Bunはい-約4000倍lsquicの64Kキャップをキャップなしで置き換え
h3 (hyperium)はい報告済み、未修正スレッドパニックHeaderMap::appendが32,768エントリでパニック
Google quicheいいえ--デコード中に60Kキャップを強制、違反時にSTREAM RESET
IIS (http.sys)いいえ--段階的な強制、4K QPACKテーブル、H3_EXCESSIVE_LOAD
ngtcp2 / nghttp3いいえ報告済み、ドキュメント更新-動的テーブルのデフォルト0、即時デクリメント参照;max_field_section_sizeはシグナリングのみ

quic-go

quic-goは、Caddy、Traefik、cloudflaredの基盤となるGoライブラリです。そのHTTP/3サーバーは、HEADERS フレームを1MiB(DefaultMaxHeaderBytes)に制限しますが、その制限はデコードされた出力ではなく、圧縮されたQPACKペイロードに適用されます。qpack/decoder.goのDecodeFullは、ブロック全体を一度に解凍し、[]HeaderFieldスライスを返します。これはその後、http.Headerマップに変換されます。

http.Headerマップはmap[string][]stringであり、そのキー、値のスライス、および内部ストレージは、エンコードされたヘッダーを超えるオーバーヘッドを追加します。1,000,000バイトのQPACKブロックは、160,075個のフィールドにデコードされ、GC後も結果のリクエストで約15MiBを保持し、エンコードされたサイズの約15倍になります。

quic-goは、対応するデコード済みヘッダー予算を強制することなく、圧縮されたHEADERSフレームを制限していました。

修正: quic-goは現在SETTINGS_MAX_FIELD_SECTION_SIZEを送信し、インクリメンタルQPACKデコードを使用して各エントリの後にサイズをチェックし、サーバー側でHTTP 431で中止します。

Cloudflare quiche

quicheには、1つのCVEに2つの問題がまとめられていました。

第一に、qpack/decoder.rsのdecode()は、サイズ計算にname.len() + value.len()を使用しており、RFCで義務付けられているフィールドごとの32バイトのオーバーヘッドを除外しています。これにより、攻撃者は制限が意図するよりも最大33倍多くのヘッダーを収めることができます。

max_field_section_size = 1 MiBの場合、RFCは約32,000個のヘッダー(各少なくとも33バイト)を許可します。quicheの計算では、1,000,000個の1バイトヘッダーを通過させることができます。

第二に、quicheのHTTP/3フレームパーサーは、ペイロードを受信する前に宣言されたフレーム長に基づいてバッファを事前割り当てします。攻撃者は16 MiBのSETTINGSまたはGOAWAYフレームを宣言し、即時のメモリ割り当てをトリガーしますが、その後データは送信しません。

修正: quiche 0.29.3は32バイトのオーバーヘッドを追加し、フレームの事前割り当てを修正しました。

lsquic

lsquicの脆弱性は、私が発見した中で最も深刻なものです。3つの問題が複合しています。

  1. header_out_dynamic_entryは、累積デコードサイズ制限なしで、参照ごとに割り当てとコピーを行います。
  2. es_max_header_list_sizeはデフォルトで0(無制限)であり、QPACKデコーダーには決して渡されません。
  3. 64KiBのヘッダーキャップは、ストリームごとではなく、HEADERS ブロックごとにリセットされ、lsquicは単一ストリームでDATAの前に無制限のHEADERSブロックを受け入れます。

動的テーブル参照は、この実装でより大きな増幅を生み出します。

  1. エンコーダーストリームを介して約4KiBのエントリを1つ挿入します。
  2. 各リクエストストリームで、15個の1バイトインデックス参照を含む一連のHEADERSフレームを送信します。

10MiB未満のワイヤートラフィックと50の接続で、1秒以内にResident Memoryを5GiBキャップに固定でき、cgroup memory.eventsは900以上の制限ヒットを記録しました。PoCのデフォルト(10接続、256MiBコンテナ)は、より小さなスケールで同じ効果を示します。

修正: lsquic 4.9.0(2026年7月15日)で修正され、ストリームごとにバッファリングされるデコード済みヘッダーセットをキャップするためにes_max_header_sets(サーバーデフォルト1)が追加され、アプリケーションがそれらをドレインするまでHTTP/3解析を一時停止します。

テスト中、www.litespeedtech.comを含む本番サーバーがQPACK_MAX_TABLE_CAPACITY=4096およびMAX_HEADER_LIST_SIZE=65536をアドバタイズしていることも観察しました。

Bun

BunのHTTP/3サポートは、lsquicを内部で使用していますが、lsquicの組み込みヘッダーセットコールバック(64KiB累積キャップ)を、quic.cの独自のus_quic_hsi_prepare/processで置き換えています。これはヘッダーごとのサイズ(64KiB)をチェックしますが、累積サイズはチェックしません。es_max_header_list_size=16384がSETTINGSでアドバタイズされますが、決して強制されません。

同じ動的テーブルトリック:1接続、100ストリーム、ストリームあたり最大500Kの1バイト参照を4KiBエントリに。ストリームあたりわずか60Kの参照(合計5.8MiBのワイヤートラフィック)で、2GiBコンテナをOOMキルするのに十分です。

Bunは同じ関数に2番目のバグがあり、増幅を永続的なサービス拒否に変換します。us_quic_hsi_prepareは、unsigned intでバッファ容量を追跡し、倍増させて成長させます。

デコード済みヘッダーセットが2GiBを超えると、needは2^31を超え、ncapは2^31から0に倍増します。ゼロから、ncap *= 2は常にゼロのままで、0 < needは決して偽にならず、lsquic_engine_process_conns内の無限ループになります。

単一ストリームで約516KiBのワイヤートラフィック(528,500のインデックス参照)を運ぶと、サーバーは100% CPUでフリーズし、回復せず、新しい着信リクエストをブロックします。

累積制限の欠如がオーバーフローを到達可能にしている原因であり、lsquicの標準的な64KiBキャップはそれを防ぎます。

両方の問題の修正は同じチェック(if (h->len + space > 16 * 1024) return NULL;)です。

aioquic / mitmproxy

aioquicは、mitmproxyやhypercornで使用されているPythonのHTTP/3ライブラリです。MAX_FIELD_SECTION_SIZEを設定定数として定義し、ピアから解析して保存しますが、デコード中には決してチェックしません。_decode_headers()は、すべてのデコード済みヘッダーを無条件に返します。

aioquicのフロー制御も天井なしで自動倍増します(50%消費後にlimit.value *= 2)。そのため、単一の接続で無制限のデータを配信できます。

ライブラリ自体はデコード済みリストを一時的にしか保持しないため、ヘッダーを処理後にドロップする最小限のaioquicサーバーは生き残りますが、mitmproxyはそうしません。ストリームごとにhttp.Requestを構築し、ストリームが開いている間保持します。

h3 crate (hyperium)

h3は、hyperiumプロジェクトからのRust製HTTP/3ライブラリです。h3-quinnアダプターを介してquinnと組み合わせて使用でき、tokio-quicheでも使用されています。すべてのQPACKヘッダーをVec<HeaderField>にデコードし、その後h3/src/proto/headers.rsのTryFrom<Vec<HeaderField>>を介してhttp::HeaderMapに変換します。

HeaderMap::appendはtry_append(...).expect("size overflows MAX_SIZE")を呼び出し、マップの容量を超えるとパニックが発生します。h3のデフォルト設定では、max_field_section_sizeは事実上無制限であるため、大きすぎるデコード済みヘッダーセットがこのパスに到達することを許容します。十分に小さいデコードサイズ制限を設定すると、この再現コードがマップの容量制限に達するのを防ぐことができます。結果として発生するパニックを以下に示します。

Wireshark

おかしなことに、これはquic-goのペイロードをテスト中に誤って発見されました。問題は、許可されるデコード済みヘッダーの制御されていない数と、HTTP/3 dissectorをフリーズさせて最終的にクラッシュさせた低速なtvb_compositeにありました。

なぜ発生するのか?

より難しい質問は、誰が制限の所有権を持つかです。ヘッダーのデコードと保存は、いくつかのコンポーネントが関与する可能性があります。

  • QUICは、ストリームで受信したバイトをアカウントに入れます。ヘッダーオブジェクトがそれらのバイトにどのように変換されるかについては、可視性がありません。
  • QPACKデコーダーはフィールドを再構築します。APIによっては、デコードサイズ予算を強制する場合もあれば、その責任を呼び出し元に任せる場合もあります。
  • HTTP/3レイヤーとアプリケーションは、それらのフィールドをリクエストオブジェクトに変換し、保持します。それらのコンテナには、独自のメモリコストと容量制限があります。

nghttp3のケースは、この境界を明確にしています。その設定は制限をアドバタイズしますが、アプリケーションがそれを強制する必要があります。h3は異なる境界を示しています。デコードされたフィールドはデコーダーを通過し、その後HeaderMapへの挿入中にパニックを引き起こす可能性があります。

質問は単純です。どのコンポーネントが実際に大きすぎるヘッダーを拒否するのか?その責任がアプリケーションにある場合、アプリケーションはヘッダーを保持する前にその制限を知り、強制する必要があります。

結論

小さなHEADERSフレームは、多くの作業を表す可能性があります。テストされた実装全体で、その作業は大きな割り当て、保持されたヘッダーセット、または下流のコンテナが制限に達したときのパニックに変わりました。プロトコルは既にデコード済みヘッダーサイズを計算する方法を提供していますが、受信コードがそれを強制する必要があります。

圧縮フレームサイズをチェックすることは、単なる出発点です。デコード予算には、フィールドごとのオーバーヘッドを含め、ヘッダーが生成されている間に適用され、アプリケーションが保持するものをカバーする必要があります。いくつかのプロジェクトがこれらのパスを修正しました。残りの発見は、アドバタイズされた制限が、それがバインドすることを意図していた入力に対する保護を提供しながら、どのように安心させることができるかを示しています。

影響を受けるサーバーの検出

フリート内の影響を受ける公開サーバーを見つけるには、異なるサーバーに対応するJA4Scan-QUICフィンガープリントを以下に示します。

ライブラリJA4Scan-QUIC
quic-go v0.56.0031g0hlss000_5973261134f6_6d6a17008abe_sr10sc7b63be
quiche 0.29.203100hlss200_8960528bd01f_6d6a17d04082_sr20ss7fb1bf
OLS 1.9.1 (lsquic 4.8.2)032g0h0ss200_24e1d2569964_d1082305b91e_0r100s0b3f78
Bun 1.4.3032g0h0ss200_756c1a5a0530_67f99a05b91e_s010s0fecef0
aioquic 1.7.00310000ss000_2a673bc4abf8_db4d1fe4882a_sr1s0seb0750
mitmproxy 12.2.30310000ss000_2a673bc4abf8_aee409e4882a_sr2s0seb0750
h3 0.0.8031g0hlsr200_8960528bd01f_6d6a17129383_se10s0fecef0

JA4Scanに関するブログ記事全体はこちらをお読みください: https://foxio.io/blog/introducing-ja4scan-active-server-fingerprinting-for-tls-and-quic

PoC

再現コードはhttps://github.com/FoxIO-LLC/blog_poc/tree/main/headeraccordionsで入手可能です。

タイムライン

  • 2025年11月9日 - quic-go報告
  • 2025年11月14日 - Wireshark報告
  • 2025年11月15日 - quiche報告
  • 2025年11月21日 - quic-go 0.57.0リリース
  • 2025年12月3日 - Wireshark 4.6.2リリース
  • 2025年12月4日 - h3報告
  • 2025年12月5日 - h3メンテナーが問題をクローズ - アルファライブラリ + デフォルト設定が変更される
  • 2025年12月17日 - nghttp3がmax_field_section_sizeをシグナリングのみとして文書化
  • 2026年5月24日 - Bun報告
  • 2026年7月13日 - aioquic報告
  • 2026年7月13日 - lsquic報告
  • 2026年7月14日 - quiche 0.29.3リリース
  • 2026年10月20日 - ブログ公開

参考文献