internal/http3: server header size limits Apply Server.MaxHeaderBytes. Includes commented-out support for MaxHeaderValueCount. It's probably not worth using build tags to make this conditional on go 1.27, so just leave it out for the moment. Change-Id: I6452b6aba3336d48ebb877621ebecb7e6a6a6964 Reviewed-on: https://go-review.googlesource.com/c/net/+/825264 Reviewed-by: Nicholas Husin <nsh@golang.org> Reviewed-by: Nicholas Husin <husin@google.com> LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/internal/http3/server.go b/internal/http3/server.go index b1ca199..f7058f6 100644 --- a/internal/http3/server.go +++ b/internal/http3/server.go
@@ -220,6 +220,9 @@ enc qpackEncoder dec qpackDecoder + maxHeaderBytes int64 + maxHeaderValueCount int64 + // For handling shutdown. controlStream *stream mu sync.Mutex // Guards everything below. @@ -231,11 +234,24 @@ // The baseCtx parameter is the base context for request handlers on this connection. func (s *server) newServerConn(baseCtx context.Context, qconn *quic.Conn, h http.Handler) { sc := &serverConn{ - qconn: qconn, - srv: s, - baseCtx: baseCtx, - handler: h, + qconn: qconn, + srv: s, + baseCtx: baseCtx, + handler: h, + maxHeaderBytes: int64(s.srv1.MaxHeaderBytes), + // TODO: When we only support go1.27. + //maxHeaderValueCount: int64(s.srv1.MaxHeaderValueCount), } + + // Should we permit disabling these limits? For now, we do not. + if sc.maxHeaderBytes <= 0 { + sc.maxHeaderBytes = int64(http.DefaultMaxHeaderBytes) + } + // TODO: When we only support go1.27. + // if sc.maxHeaderValueCount <= 0 { + // sc.maxHeaderValueCount = int64(http.DefaultMaxHeaderValueCount) + // } + s.registerConn(sc) defer s.unregisterConn(sc) sc.enc.init() @@ -247,7 +263,9 @@ if err != nil { return } - sc.controlStream.writeSettings() + sc.controlStream.writeSettings( + settingsMaxFieldSectionSize, sc.maxHeaderBytes, + ) sc.controlStream.Flush() sc.acceptStreams(sc.qconn, sc) @@ -362,11 +380,28 @@ if ftype != frameTypeHeaders { return nil, pseudoHeader{}, &streamError{errH3MessageError, "received other frames when expecting HEADERS"} } + if st.lim > sc.maxHeaderBytes { + // If the encoded headers exceed the limit, just reject the request out of hand. + // This lets us safely check limits in the dec.decode callback below, + // since the maximum Huffman expansion factor is only ~1.6x. + return nil, pseudoHeader{}, &streamError{errH3RequestRejected, "headers too large"} + } header := make(http.Header) + valueCount := int64(0) + totalSize := int64(0) var pHeader pseudoHeader var dec qpackDecoder var hasMethod, hasScheme, hasPath, hasAuthority bool if err := dec.decode(st, func(_ indexType, name, value string) error { + totalSize += int64(len(name)) + int64(len(value)) + 32 // RFC 9114 Section 4.2.2 + valueCount++ + if totalSize > sc.maxHeaderBytes { + return &streamError{errH3RequestRejected, "headers too large"} + } + // TODO: When we only support go1.27. + //if valueCount > sc.maxHeaderValueCount { + // return &streamError{errH3RequestRejected, "headers too large"} + //} if !httpguts.ValidHeaderFieldValue(value) { return &streamError{errH3MessageError, "invalid field value"} }
diff --git a/internal/http3/server_test.go b/internal/http3/server_test.go index e074cb0..0c2776a 100644 --- a/internal/http3/server_test.go +++ b/internal/http3/server_test.go
@@ -16,6 +16,7 @@ "reflect" "slices" "strconv" + "strings" "sync" "testing" "testing/synctest" @@ -542,6 +543,72 @@ }) } +func TestServerHeaderLimits(t *testing.T) { + for _, test := range []struct { + name string + h http.Header + valid bool + maxHeaderBytes int + maxHeaderValueCount int + }{{ + name: "within limits", + h: http.Header{ + "x-foo": {strings.Repeat("x", 1000)}, + }, + maxHeaderBytes: 1500, + valid: true, + }, { + name: "too many header bytes", + h: http.Header{ + "x-foo": {strings.Repeat("x", 1000)}, + "x-bar": {strings.Repeat("x", 1000)}, + }, + maxHeaderBytes: 1500, + }, { + name: "field count within limit", + h: http.Header{ + // :method, :scheme, :path, plus: + "x-foo": {"4"}, + "x-bar": {"5"}, + }, + maxHeaderBytes: 1500, + maxHeaderValueCount: 5, + valid: true, + }, { + name: "field count over limit", + h: http.Header{ + // :method, :scheme, :path, plus: + "x-foo": {"4"}, + "x-bar": {"5"}, + }, + maxHeaderBytes: 1500, + maxHeaderValueCount: 4, + }} { + synctestSubtest(t, test.name, func(t *testing.T) { + if test.maxHeaderValueCount != 0 { + t.Skip("TODO: when we support only go1.27") + } + ts := newTestServer(t, nil) + ts.s.srv1.MaxHeaderBytes = test.maxHeaderBytes + // TODO: When we only support go1.27. + //ts.s.srv1.MaxHeaderValueCount = test.maxHeaderValueCount + tc := ts.connect() + tc.greet() + + reqStream := tc.newStream(streamTypeRequest) + reqStream.writeHeaders(requestHeader(test.h)) + if test.valid { + call := tc.nextHandlerCall() + if call == nil { + t.Fatal("no server handler call; want one") + } + } else { + reqStream.wantError(quic.StreamErrorCode(errH3RequestRejected)) + } + }) + } +} + func TestServerBody(t *testing.T) { synctest.Test(t, func(t *testing.T) { ts := newTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {