net/http.Server にリクエストヘッダー値の個数上限を設定できる MaxHeaderValueCount フィールドを追加し、デフォルトで500件(DefaultMaxHeaderValueCount)に制限することで、大量の小さなヘッダーを送りつけるDoS攻撃を防げるようにした。
net/http
概要
Server.MaxHeaderValueCount が新設され、サーバーが1リクエストから受け入れるヘッダー値の個数に上限を設定できるようになった。ゼロ値のときは新設の定数 DefaultMaxHeaderValueCount(500)が使われる。従来の MaxHeaderBytes はヘッダーの合計バイト数しか制限できなかったが、本フィールドによってヘッダーの「個数」を独立して制限できるようになる。
導入経緯
net/http.Server が提供するヘッダー制限手段は、これまで合計バイト数を制限する MaxHeaderBytes のみだった。しかし、1バイトの名前・0バイトの値を持つヘッダーを大量に送りつける、いわゆる「HTTP/2 Bomb」攻撃(参考記事)のように、サーバー内部でのヘッダー表現がワイヤ上のバイト数を大きく上回る(メモリ増幅)攻撃手法が問題視されるようになった。Goの net/http HTTP/2実装は各ヘッダー値に32バイトのオーバーヘッドを見込んで MaxHeaderBytes を計算しているためこの特定の攻撃には強いが、SSOのCookieなど正当に大きなヘッダーを扱う必要があるユーザーは、MaxHeaderBytes を大きくせざるを得ず、結果として個数ベースの攻撃に対して脆弱になりやすいという問題があった。
提案者は、ヘッダーの「サイズ」と「個数」を独立に制限できる Server.MaxHeaderValueCount の追加を提案した(issue #79936)。提案では併せて暫定策として GODEBUG=httpmaxheadervalues=N の追加も提案されていたが、proposal committeeでの議論の結果、フリーズ例外を適用してGo 1.27に直接 MaxHeaderValueCount を取り込む方針となり、GODEBUGは見送られた。議論では、ヘッダー値の個数(MaxHeaderNameCount ではなく MaxHeaderValueCount)を制限対象とすることで、1つのヘッダー名に大量の空値を付与する攻撃パターンにも対応できる設計が採用されている。
なお、Transportにも同様の MaxResponseHeaderValueCount を追加してクライアント側のレスポンスヘッダー爆弾を防ぐべきだという意見もissue上で出されたが、今回mergeされた実装には含まれていない。
議論のハイライト
- @aclements から「
MaxHeaderBytesを大きくしても、攻撃者が1つの巨大なヘッダーを送るだけでは」という疑問が出された。これに対し提案者は、①大きなヘッダー1本による攻撃はワイヤ上でもほぼ同じ量のデータ送信を強いられ非対称性がないためDoSとして成立しにくい、②ヘッダーは内部でmap[string][]stringとして保持されるため、少数の大きな値より多数の小さな値のほうがcanonicalize処理やGCのコストが大きくなる、と説明した。 - トレーラーヘッダーへの適用範囲は議論時点では未確定とされていた。HTTP/1は従来
MaxHeaderBytesをトレーラーに適用しておらず約4KBの静的な上限のみだったが、HTTP/2は適用済みという非対称な状況だった。 - フリーズ例外が承認され、Go 1.27での取り込みが決定した。
使用例
Before
package main
import (
"log"
"net/http"
)
func main() {
// MaxHeaderBytes だけでは、大きなCookieを許容しつつ
// 小さなヘッダーの大量送信を防ぐことができない。
srv := &http.Server{
Addr: ":8080",
MaxHeaderBytes: 1 << 20, // 1MB
}
log.Fatal(srv.ListenAndServe())
}
After
package main
import (
"log"
"net/http"
)
func main() {
// MaxHeaderBytes でヘッダーの合計サイズを許容しつつ、
// MaxHeaderValueCount でヘッダー値の個数を絞ることで
// 個数ベースのDoS攻撃を防げる。
srv := &http.Server{
Addr: ":8080",
MaxHeaderBytes: 1 << 20, // 1MB(大きなCookie等を許容)
MaxHeaderValueCount: 100, // ヘッダー値は100個までに制限
}
log.Fatal(srv.ListenAndServe())
}
移行時の注意
MaxHeaderValueCount を明示的に設定していないサーバーでも、Go 1.27以降はデフォルト値 DefaultMaxHeaderValueCount(500)が自動的に適用される。プロキシ経由などでヘッダー値が500個を超えるような正当なリクエストを受け付けているサーバーは、Go 1.27へのアップデートでリクエストが 431 Request Header Fields Too Large で拒否されるようになる可能性があるため、必要に応じて Server.MaxHeaderValueCount を明示的に大きい値に設定する必要がある。また、カンマ区切りで複数の値を持つ1本のヘッダー行は1個として数えられる一方、同名ヘッダーを複数行に分けて送った場合はその行数分カウントされる点にも注意が必要である。
実装解説
実装は net/http.Server に定数とフィールドを追加し(DefaultMaxHeaderValueCount・maxHeaderValueCount()、server.go;l=941、server.go;l=3144)、その値をHTTP/1・HTTP/2それぞれのヘッダーパース経路に伝播する形になっている。
HTTP/1では、readRequestLimit が maxHeaderValueCount() の値を maxHeaders として net/textproto.readMIMEHeader に linkname 経由で渡し(server.go;l=1059、request.go;l=1081)、パース段階でヘッダー値の個数を数えて制限する。上限超過時は errTooLarge が返り、接続ハンドラ側で 431 Request Header Fields Too Large を返してから接続を閉じる(server.go;l=2089)。同じ上限値は readTransfer を通じてトレーラーヘッダーの読み取り(body.readTrailer)にも渡されており(transfer.go;l=825、transfer.go;l=946)、HTTP/1のトレーラーヘッダーにも同じ個数制限が及ぶようになっている。
HTTP/2では、http2ServerConfig.MaxHeaderValueCount() が Server.maxHeaderValueCount() の値をそのまま internal/http2 パッケージへ橋渡しし(http2.go;l=190)、HPACKデコード時のコールバック内で headerCount をインクリメントしながら上限と比較する形で実装されている。上限を超えると以降のフィールドの取り込みを止め、Truncated フラグを立てる(frame.go;l=1739)。