VYPR
Moderate severityNVD Advisory· Published Dec 11, 2025· Updated Dec 12, 2025

quic-go HTTP/3 QPACK Header Expansion DoS

CVE-2025-64702

Description

quic-go is an implementation of the QUIC protocol in Go. Versions 0.56.0 and below are vulnerable to excessive memory allocation through quic-go's HTTP/3 client and server implementations by sending a QPACK-encoded HEADERS frame that decodes into a large header field section (many unique header names and/or large values). The implementation builds an http.Header (used on the http.Request and http.Response, respectively), while only enforcing limits on the size of the (QPACK-compressed) HEADERS frame, but not on the decoded header, leading to memory exhaustion. This issue is fixed in version 0.57.0.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

A memory exhaustion vulnerability in quic-go before 0.57.0 allows remote attackers to cause denial of service via QPACK-encoded HTTP/3 HEADERS frames that expand to large decoded headers.

Root

Cause

The vulnerability resides in quic-go's handling of QPACK-encoded HTTP/3 HEADERS frames [1][4]. The implementation limits the size of the compressed HEADERS frame (via MaxHeaderBytes), but does not enforce a limit on the size after QPACK decompression [2][4]. A crafted QPACK static table entry can expand to roughly 50 times its compressed size, leading to excessive memory allocation when building the http.Header map for http.Request or http.Response [4].

Attack

Vector

An unauthenticated remote attacker can send a malicious HTTP/3 HEADERS frame to a vulnerable server or client [2][4]. No special network position is required beyond being able to establish a QUIC connection and send HTTP/3 frames. The attack works symmetrically: both server and client construct the same http.Header structure, making both sides equally exposed [4]. The compressed frame may pass the enforced size limits, while the decoded version exhausts memory.

Impact

Successful exploitation causes excessive memory allocation, leading to memory exhaustion on the target host [2][4]. This results in a denial-of-service (DoS) condition, potentially crashing the application or making it unresponsive [2][4]. The vulnerability affects all versions of quic-go up to and including 0.56.0 [2].

Mitigation

The fix is implemented in quic-go version 0.57.0 [2]. The patch introduces enforcement of the decoded field section size limits per RFC 9114, sending SETTINGS_MAX_FIELD_SECTION_SIZE and performing incremental QPACK decoding to abort early when the header size exceeds the limit [1][4]. Users should upgrade to version 0.57.0 or later. No workaround is documented for older versions.

AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/quic-go/quic-goGo
< 0.57.00.57.0

Affected products

2
  • Quic Go/Quic Gollm-fuzzy
    Range: <=0.56.0
  • quic-go/quic-gov5
    Range: < 0.57.0

Patches

1
5b2d2129f831

http3: limit size of decompressed headers (#5452)

https://github.com/quic-go/quic-goMarten SeemannNov 21, 2025via ghsa
7 files changed · +198 37
  • http3/conn.go+1 1 modified
    @@ -196,7 +196,7 @@ func (c *Conn) openRequestStream(
     func (c *Conn) decodeTrailers(r io.Reader, streamID quic.StreamID, hf *headersFrame, maxHeaderBytes int) (http.Header, error) {
     	if hf.Length > uint64(maxHeaderBytes) {
     		maybeQlogInvalidHeadersFrame(c.qlogger, streamID, hf.Length)
    -		return nil, fmt.Errorf("HEADERS frame too large: %d bytes (max: %d)", hf.Length, maxHeaderBytes)
    +		return nil, fmt.Errorf("http3: HEADERS frame too large: %d bytes (max: %d)", hf.Length, maxHeaderBytes)
     	}
     
     	b := make([]byte, hf.Length)
    
  • http3/headers.go+14 5 modified
    @@ -20,6 +20,8 @@ type qpackError struct{ err error }
     func (e *qpackError) Error() string { return fmt.Sprintf("qpack: %v", e.err) }
     func (e *qpackError) Unwrap() error { return e.err }
     
    +var errHeaderTooLarge = errors.New("http3: headers too large")
    +
     type header struct {
     	// Pseudo header fields defined in RFC 9114
     	Path      string
    @@ -44,7 +46,7 @@ var invalidHeaderFields = [...]string{
     	"upgrade",
     }
     
    -func parseHeaders(decodeFn qpack.DecodeFunc, isRequest bool, headerFields *[]qpack.HeaderField) (header, error) {
    +func parseHeaders(decodeFn qpack.DecodeFunc, isRequest bool, sizeLimit int, headerFields *[]qpack.HeaderField) (header, error) {
     	hdr := header{Headers: make(http.Header)}
     	var readFirstRegularHeader, readContentLength bool
     	var contentLengthStr string
    @@ -59,6 +61,13 @@ func parseHeaders(decodeFn qpack.DecodeFunc, isRequest bool, headerFields *[]qpa
     		if headerFields != nil {
     			*headerFields = append(*headerFields, h)
     		}
    +		// RFC 9114, section 4.2.2:
    +		// The size of a field list is calculated based on the uncompressed size of fields,
    +		// including the length of the name and value in bytes plus an overhead of 32 bytes for each field.
    +		sizeLimit -= len(h.Name) + len(h.Value) + 32
    +		if sizeLimit < 0 {
    +			return header{}, errHeaderTooLarge
    +		}
     		// field names need to be lowercase, see section 4.2 of RFC 9114
     		if strings.ToLower(h.Name) != h.Name {
     			return header{}, fmt.Errorf("header field is not lower-case: %s", h.Name)
    @@ -167,8 +176,8 @@ func parseTrailers(decodeFn qpack.DecodeFunc, headerFields *[]qpack.HeaderField)
     	return h, nil
     }
     
    -func requestFromHeaders(decodeFn qpack.DecodeFunc, headerFields *[]qpack.HeaderField) (*http.Request, error) {
    -	hdr, err := parseHeaders(decodeFn, true, headerFields)
    +func requestFromHeaders(decodeFn qpack.DecodeFunc, sizeLimit int, headerFields *[]qpack.HeaderField) (*http.Request, error) {
    +	hdr, err := parseHeaders(decodeFn, true, sizeLimit, headerFields)
     	if err != nil {
     		return nil, err
     	}
    @@ -241,8 +250,8 @@ func requestFromHeaders(decodeFn qpack.DecodeFunc, headerFields *[]qpack.HeaderF
     // using the decoded qpack header filed.
     // It is only called for the HTTP header (and not the HTTP trailer).
     // It takes an http.Response as an argument to allow the caller to set the trailer later on.
    -func updateResponseFromHeaders(rsp *http.Response, decodeFn qpack.DecodeFunc, headerFields *[]qpack.HeaderField) error {
    -	hdr, err := parseHeaders(decodeFn, false, headerFields)
    +func updateResponseFromHeaders(rsp *http.Response, decodeFn qpack.DecodeFunc, sizeLimit int, headerFields *[]qpack.HeaderField) error {
    +	hdr, err := parseHeaders(decodeFn, false, sizeLimit, headerFields)
     	if err != nil {
     		return err
     	}
    
  • http3/headers_test.go+21 20 modified
    @@ -4,6 +4,7 @@ import (
     	"bytes"
     	"fmt"
     	"io"
    +	"math"
     	"net/http"
     	"testing"
     
    @@ -41,7 +42,7 @@ func testRequestHeaderParsing(t *testing.T, path string) {
     		{Name: ":method", Value: http.MethodGet},
     		{Name: "content-length", Value: "42"},
     	}
    -	req, err := requestFromHeaders(decodeFromSlice(headers), nil)
    +	req, err := requestFromHeaders(decodeFromSlice(headers), math.MaxInt, nil)
     	require.NoError(t, err)
     	require.Equal(t, http.MethodGet, req.Method)
     	require.Equal(t, path, req.URL.Path)
    @@ -64,7 +65,7 @@ func TestRequestHeadersContentLength(t *testing.T) {
     			{Name: ":authority", Value: "quic-go.net"},
     			{Name: ":method", Value: http.MethodGet},
     		}
    -		req, err := requestFromHeaders(decodeFromSlice(headers), nil)
    +		req, err := requestFromHeaders(decodeFromSlice(headers), math.MaxInt, nil)
     		require.NoError(t, err)
     		require.Equal(t, int64(-1), req.ContentLength)
     	})
    @@ -77,7 +78,7 @@ func TestRequestHeadersContentLength(t *testing.T) {
     			{Name: "content-length", Value: "42"},
     			{Name: "content-length", Value: "42"},
     		}
    -		req, err := requestFromHeaders(decodeFromSlice(headers), nil)
    +		req, err := requestFromHeaders(decodeFromSlice(headers), math.MaxInt, nil)
     		require.NoError(t, err)
     		require.Equal(t, "42", req.Header.Get("Content-Length"))
     	})
    @@ -107,7 +108,7 @@ func TestRequestHeadersContentLengthValidation(t *testing.T) {
     		},
     	} {
     		t.Run(tc.name, func(t *testing.T) {
    -			_, err := requestFromHeaders(decodeFromSlice(tc.headers), nil)
    +			_, err := requestFromHeaders(decodeFromSlice(tc.headers), math.MaxInt, nil)
     			if tc.errContains != "" {
     				require.ErrorContains(t, err, tc.errContains)
     			}
    @@ -228,7 +229,7 @@ func TestRequestHeadersValidation(t *testing.T) {
     		},
     	} {
     		t.Run(tc.name, func(t *testing.T) {
    -			_, err := requestFromHeaders(decodeFromSlice(tc.headers), nil)
    +			_, err := requestFromHeaders(decodeFromSlice(tc.headers), math.MaxInt, nil)
     			require.EqualError(t, err, tc.err)
     			require.NotErrorAs(t, err, new(*qpackError))
     		})
    @@ -243,7 +244,7 @@ func TestCookieHeader(t *testing.T) {
     		{Name: "cookie", Value: "cookie1=foobar1"},
     		{Name: "cookie", Value: "cookie2=foobar2"},
     	}
    -	req, err := requestFromHeaders(decodeFromSlice(headers), nil)
    +	req, err := requestFromHeaders(decodeFromSlice(headers), math.MaxInt, nil)
     	require.NoError(t, err)
     	require.Equal(t, http.Header{
     		"Cookie": []string{"cookie1=foobar1; cookie2=foobar2"},
    @@ -259,7 +260,7 @@ func TestHeadersConcatenation(t *testing.T) {
     		{Name: "duplicate-header", Value: "1"},
     		{Name: "duplicate-header", Value: "2"},
     	}
    -	req, err := requestFromHeaders(decodeFromSlice(headers), nil)
    +	req, err := requestFromHeaders(decodeFromSlice(headers), math.MaxInt, nil)
     	require.NoError(t, err)
     	require.Equal(t, http.Header{
     		"Cache-Control":    []string{"max-age=0"},
    @@ -272,7 +273,7 @@ func TestRequestHeadersConnect(t *testing.T) {
     		{Name: ":authority", Value: "quic-go.net"},
     		{Name: ":method", Value: http.MethodConnect},
     	}
    -	req, err := requestFromHeaders(decodeFromSlice(headers), nil)
    +	req, err := requestFromHeaders(decodeFromSlice(headers), math.MaxInt, nil)
     	require.NoError(t, err)
     	require.Equal(t, http.MethodConnect, req.Method)
     	require.Equal(t, "HTTP/3.0", req.Proto)
    @@ -302,7 +303,7 @@ func TestRequestHeadersConnectValidation(t *testing.T) {
     		},
     	} {
     		t.Run(tc.name, func(t *testing.T) {
    -			_, err := requestFromHeaders(decodeFromSlice(tc.headers), nil)
    +			_, err := requestFromHeaders(decodeFromSlice(tc.headers), math.MaxInt, nil)
     			require.EqualError(t, err, tc.err)
     		})
     	}
    @@ -316,7 +317,7 @@ func TestRequestHeadersExtendedConnect(t *testing.T) {
     		{Name: ":authority", Value: "quic-go.net"},
     		{Name: ":path", Value: "/foo?val=1337"},
     	}
    -	req, err := requestFromHeaders(decodeFromSlice(headers), nil)
    +	req, err := requestFromHeaders(decodeFromSlice(headers), math.MaxInt, nil)
     	require.NoError(t, err)
     	require.Equal(t, http.MethodConnect, req.Method)
     	require.Equal(t, "webtransport", req.Proto)
    @@ -331,7 +332,7 @@ func TestRequestHeadersExtendedConnectRequestValidation(t *testing.T) {
     		{Name: ":authority", Value: "quic.clemente.io"},
     		{Name: ":path", Value: "/foo"},
     	}
    -	_, err := requestFromHeaders(decodeFromSlice(headers), nil)
    +	_, err := requestFromHeaders(decodeFromSlice(headers), math.MaxInt, nil)
     	require.EqualError(t, err, "extended CONNECT: :scheme, :path and :authority must not be empty")
     }
     
    @@ -341,7 +342,7 @@ func TestResponseHeaderParsing(t *testing.T) {
     		{Name: "content-length", Value: "42"},
     	}
     	rsp := &http.Response{}
    -	require.NoError(t, updateResponseFromHeaders(rsp, decodeFromSlice(headers), nil))
    +	require.NoError(t, updateResponseFromHeaders(rsp, decodeFromSlice(headers), math.MaxInt, nil))
     	require.Equal(t, "HTTP/3.0", rsp.Proto)
     	require.Equal(t, 3, rsp.ProtoMajor)
     	require.Zero(t, rsp.ProtoMinor)
    @@ -391,7 +392,7 @@ func TestResponseHeaderParsingValidation(t *testing.T) {
     		},
     	} {
     		t.Run(tc.name, func(t *testing.T) {
    -			err := updateResponseFromHeaders(&http.Response{}, decodeFromSlice(tc.headers), nil)
    +			err := updateResponseFromHeaders(&http.Response{}, decodeFromSlice(tc.headers), math.MaxInt, nil)
     			if tc.errContains != "" {
     				require.ErrorContains(t, err, tc.errContains)
     			}
    @@ -416,7 +417,7 @@ func TestResponseHeaderParsingValidation(t *testing.T) {
     				{Name: ":status", Value: "404"},
     				{Name: tc.invalidField, Value: "some-value"},
     			}
    -			err := updateResponseFromHeaders(&http.Response{}, decodeFromSlice(headers), nil)
    +			err := updateResponseFromHeaders(&http.Response{}, decodeFromSlice(headers), math.MaxInt, nil)
     			require.EqualError(t, err, fmt.Sprintf("invalid header field name: %q", tc.invalidField))
     		})
     	}
    @@ -429,7 +430,7 @@ func TestResponseTrailerFields(t *testing.T) {
     		{Name: "trailer", Value: "TRAILER3"},
     	}
     	var rsp http.Response
    -	require.NoError(t, updateResponseFromHeaders(&rsp, decodeFromSlice(headers), nil))
    +	require.NoError(t, updateResponseFromHeaders(&rsp, decodeFromSlice(headers), math.MaxInt, nil))
     	require.Equal(t, 0, len(rsp.Header))
     	require.Equal(t, http.Header(map[string][]string{
     		"Trailer1": nil,
    @@ -443,13 +444,13 @@ func TestResponseTrailerParsingTE(t *testing.T) {
     		{Name: ":status", Value: "404"},
     		{Name: "te", Value: "trailers"},
     	}
    -	require.NoError(t, updateResponseFromHeaders(&http.Response{}, decodeFromSlice(headers), nil))
    +	require.NoError(t, updateResponseFromHeaders(&http.Response{}, decodeFromSlice(headers), math.MaxInt, nil))
     	headers = []qpack.HeaderField{
     		{Name: ":status", Value: "404"},
     		{Name: "te", Value: "not-trailers"},
     	}
     	require.EqualError(t,
    -		updateResponseFromHeaders(&http.Response{}, decodeFromSlice(headers), nil),
    +		updateResponseFromHeaders(&http.Response{}, decodeFromSlice(headers), math.MaxInt, nil),
     		`invalid TE header field value: "not-trailers"`)
     }
     
    @@ -478,14 +479,14 @@ func TestQpackError(t *testing.T) {
     	t.Run("header parsing", func(t *testing.T) {
     		dec := qpack.NewDecoder()
     		decodeFn := dec.Decode(buf.Bytes()[:len(buf.Bytes())/2])
    -		_, err := requestFromHeaders(decodeFn, nil)
    +		_, err := requestFromHeaders(decodeFn, math.MaxInt, nil)
     		require.ErrorAs(t, err, new(*qpackError))
     	})
     
     	t.Run("trailer parsing", func(t *testing.T) {
     		dec := qpack.NewDecoder()
     		decodeFn := dec.Decode(buf.Bytes()[:len(buf.Bytes())/2])
    -		_, err := parseTrailers(decodeFn, nil)
    +		err := updateResponseFromHeaders(&http.Response{}, decodeFn, math.MaxInt, nil)
     		require.ErrorAs(t, err, new(*qpackError))
     	})
     }
    @@ -517,7 +518,7 @@ func BenchmarkRequestFromHeaders(b *testing.B) {
     	dec := qpack.NewDecoder()
     	for b.Loop() {
     		decodeFn := dec.Decode(buf.Bytes())
    -		if _, err := requestFromHeaders(decodeFn, nil); err != nil {
    +		if _, err := requestFromHeaders(decodeFn, math.MaxInt, nil); err != nil {
     			b.Fatalf("failed to parse request: %v", err)
     		}
     	}
    
  • http3/server.go+25 7 modified
    @@ -572,16 +572,16 @@ func (s *Server) handleConn(conn *quic.Conn) error {
     	return handleErr
     }
     
    -func (s *Server) maxHeaderBytes() uint64 {
    +func (s *Server) maxHeaderBytes() int {
     	if s.MaxHeaderBytes <= 0 {
     		return http.DefaultMaxHeaderBytes
     	}
    -	return uint64(s.MaxHeaderBytes)
    +	return s.MaxHeaderBytes
     }
     
     func (s *Server) handleRequest(
     	conn *Conn,
    -	str datagramStream,
    +	str *stateTrackingStream,
     	decoder *qpack.Decoder,
     	qlogger qlogwriter.Recorder,
     ) {
    @@ -610,10 +610,12 @@ func (s *Server) handleRequest(
     		conn.CloseWithError(quic.ApplicationErrorCode(ErrCodeFrameUnexpected), "expected first frame to be a HEADERS frame")
     		return
     	}
    -	if hf.Length > s.maxHeaderBytes() {
    +	if hf.Length > uint64(s.maxHeaderBytes()) {
     		maybeQlogInvalidHeadersFrame(qlogger, str.StreamID(), hf.Length)
    -		str.CancelRead(quic.StreamErrorCode(ErrCodeFrameError))
    -		str.CancelWrite(quic.StreamErrorCode(ErrCodeFrameError))
    +		// stop the client from sending more data
    +		str.CancelRead(quic.StreamErrorCode(ErrCodeExcessiveLoad))
    +		// send a 431 Response (Request Header Fields Too Large)
    +		s.rejectWithHeaderFieldsTooLarge(str, conn, qlogger)
     		return
     	}
     	headerBlock := make([]byte, hf.Length)
    @@ -628,11 +630,19 @@ func (s *Server) handleRequest(
     	if qlogger != nil {
     		hfs = make([]qpack.HeaderField, 0, 16)
     	}
    -	req, err := requestFromHeaders(decodeFn, &hfs)
    +	req, err := requestFromHeaders(decodeFn, s.maxHeaderBytes(), &hfs)
     	if qlogger != nil {
     		qlogParsedHeadersFrame(qlogger, str.StreamID(), hf, hfs)
     	}
     	if err != nil {
    +		if errors.Is(err, errHeaderTooLarge) {
    +			// stop the client from sending more data
    +			str.CancelRead(quic.StreamErrorCode(ErrCodeExcessiveLoad))
    +			// send a 431 Response (Request Header Fields Too Large)
    +			s.rejectWithHeaderFieldsTooLarge(str, conn, qlogger)
    +			return
    +		}
    +
     		errCode := ErrCodeMessageError
     		var qpackErr *qpackError
     		if errors.As(err, &qpackErr) {
    @@ -719,6 +729,14 @@ func (s *Server) handleRequest(
     	str.Close()
     }
     
    +func (s *Server) rejectWithHeaderFieldsTooLarge(str *stateTrackingStream, conn *Conn, qlogger qlogwriter.Recorder) {
    +	hstr := newStream(str, conn, nil, nil, qlogger)
    +	defer hstr.Close()
    +	r := newResponseWriter(hstr, conn, false, s.Logger)
    +	r.WriteHeader(http.StatusRequestHeaderFieldsTooLarge)
    +	r.Flush()
    +}
    +
     // Close the server immediately, aborting requests and sending CONNECTION_CLOSE frames to connected clients.
     // Close in combination with ListenAndServe() (instead of Serve()) may race if it is called before a UDP socket is established.
     // It is the caller's responsibility to close any connection passed to ServeQUICConn.
    
  • http3/server_test.go+3 2 modified
    @@ -415,8 +415,9 @@ func testServerRequestHeaderTooLarge(t *testing.T, req *http.Request, maxHeaderB
     
     	go s.ServeQUICConn(serverConn)
     
    -	expectStreamReadReset(t, str, quic.StreamErrorCode(ErrCodeFrameError))
    -	expectStreamWriteReset(t, str, quic.StreamErrorCode(ErrCodeFrameError))
    +	hfs := decodeHeader(t, str)
    +	require.Equal(t, []string{"431"}, hfs[":status"])
    +	expectStreamWriteReset(t, str, quic.StreamErrorCode(ErrCodeExcessiveLoad))
     	require.False(t, called)
     }
     
    
  • http3/stream.go+1 1 modified
    @@ -348,7 +348,7 @@ func (s *RequestStream) ReadResponse() (*http.Response, error) {
     		hfs = make([]qpack.HeaderField, 0, 16)
     	}
     	res := s.response
    -	err = updateResponseFromHeaders(res, decodeFn, &hfs)
    +	err = updateResponseFromHeaders(res, decodeFn, s.maxHeaderBytes, &hfs)
     	if s.str.qlogger != nil {
     		qlogParsedHeadersFrame(s.str.qlogger, s.str.StreamID(), hf, hfs)
     	}
    
  • integrationtests/self/http_test.go+133 1 modified
    @@ -25,7 +25,9 @@ import (
     
     	"github.com/quic-go/quic-go"
     	"github.com/quic-go/quic-go/http3"
    +	"github.com/quic-go/quic-go/http3/qlog"
     	quicproxy "github.com/quic-go/quic-go/integrationtests/tools/proxy"
    +	"github.com/quic-go/quic-go/testutils/events"
     
     	"github.com/stretchr/testify/assert"
     	"github.com/stretchr/testify/require"
    @@ -79,12 +81,15 @@ func startHTTPServer(t *testing.T, mux *http.ServeMux, opts ...func(*http3.Serve
     	return conn.LocalAddr().(*net.UDPAddr).Port
     }
     
    -func newHTTP3Client(t *testing.T) *http.Client {
    +func newHTTP3Client(t *testing.T, opts ...func(*http3.Transport)) *http.Client {
     	tr := &http3.Transport{
     		TLSClientConfig:    getTLSClientConfigWithoutServerName(),
     		QUICConfig:         getQuicConfig(&quic.Config{MaxIdleTimeout: 10 * time.Second}),
     		DisableCompression: true,
     	}
    +	for _, opt := range opts {
    +		opt(tr)
    +	}
     	addDialCallback(t, tr)
     	t.Cleanup(func() { tr.Close() })
     	return &http.Client{Transport: tr}
    @@ -243,6 +248,133 @@ func TestHTTPHeaders(t *testing.T) {
     	require.Equal(t, echoHdr, resp.Header.Get("echo"))
     }
     
    +func TestHTTPHeaderSizeLimitServer(t *testing.T) {
    +	t.Run("large HEADERS frame", func(t *testing.T) {
    +		const limit = 1024
    +		hdr := make(http.Header)
    +		for range 20 {
    +			hdr.Add(randomString(50), randomString(50))
    +		}
    +		headersFrameSize := testHTTPHeaderSizeLimitServer(t, hdr, limit)
    +		require.Greater(t, headersFrameSize, limit)
    +	})
    +
    +	t.Run("large decompressed HEADERS frame", func(t *testing.T) {
    +		const limit = 1024
    +		hdr := make(http.Header)
    +		for range 200 {
    +			// This is a QPACK static table entry, so it will be compressed.
    +			hdr.Add("content-type", "text/plain;charset=utf-8")
    +		}
    +		headersFrameSize := testHTTPHeaderSizeLimitServer(t, hdr, limit)
    +		require.Less(t, headersFrameSize, limit)
    +	})
    +}
    +
    +func testHTTPHeaderSizeLimitServer(t *testing.T, hdr http.Header, limit int) (headersFrameSize int) {
    +	mux := http.NewServeMux()
    +	var handlerCalled bool
    +	mux.HandleFunc("/headers", func(w http.ResponseWriter, r *http.Request) {
    +		handlerCalled = true
    +	})
    +	port := startHTTPServer(t, mux, func(s *http3.Server) { s.MaxHeaderBytes = limit })
    +
    +	var eventRecorder events.Recorder
    +	cl := newHTTP3Client(t, func(tr *http3.Transport) {
    +		tr.QUICConfig = getQuicConfig(&quic.Config{
    +			MaxIdleTimeout: 10 * time.Second,
    +			Tracer:         newTracer(&eventRecorder),
    +		})
    +	})
    +
    +	req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("https://localhost:%d/headers", port), nil)
    +	require.NoError(t, err)
    +	req.Header = hdr
    +
    +	resp, err := cl.Do(req)
    +	require.NoError(t, err)
    +	require.Equal(t, http.StatusRequestHeaderFieldsTooLarge, resp.StatusCode)
    +	require.False(t, handlerCalled)
    +
    +	for _, ev := range eventRecorder.Events(qlog.FrameCreated{}) {
    +		fc := ev.(qlog.FrameCreated)
    +		if _, ok := fc.Frame.Frame.(qlog.HeadersFrame); ok {
    +			headersFrameSize = fc.Raw.Length
    +			break
    +		}
    +	}
    +	return headersFrameSize
    +}
    +
    +func TestHTTPHeaderSizeLimitClient(t *testing.T) {
    +	t.Run("large HEADERS frame", func(t *testing.T) {
    +		const limit = 1024
    +		hdr := make(http.Header)
    +		for range 20 {
    +			hdr.Add(randomString(50), randomString(50))
    +		}
    +		headersFrameSize, requestErr := testHTTPHeaderSizeLimitClient(t, hdr, limit)
    +		require.ErrorContains(t, requestErr, "http3: HEADERS frame too large")
    +		require.Greater(t, headersFrameSize, limit)
    +	})
    +
    +	t.Run("large decompressed HEADERS frame", func(t *testing.T) {
    +		const limit = 1024
    +		hdr := make(http.Header)
    +		for range 200 {
    +			// This is a QPACK static table entry, so it will be compressed.
    +			hdr.Add("content-type", "text/plain;charset=utf-8")
    +		}
    +		headersFrameSize, requestErr := testHTTPHeaderSizeLimitClient(t, hdr, limit)
    +		require.ErrorContains(t, requestErr, "http3: headers too large")
    +		require.Less(t, headersFrameSize, limit)
    +	})
    +}
    +
    +func testHTTPHeaderSizeLimitClient(t *testing.T, hdr http.Header, limit int) (headersFrameSize int, requestErr error) {
    +	mux := http.NewServeMux()
    +	var handlerCalled atomic.Bool
    +	mux.HandleFunc("/headers", func(w http.ResponseWriter, r *http.Request) {
    +		handlerCalled.Store(true)
    +		for k, v := range hdr {
    +			for _, val := range v {
    +				w.Header().Add(k, val)
    +			}
    +		}
    +	})
    +	port := startHTTPServer(t, mux)
    +
    +	var eventRecorder events.Recorder
    +	cl := newHTTP3Client(t,
    +		func(tr *http3.Transport) {
    +			tr.MaxResponseHeaderBytes = limit
    +			tr.QUICConfig = getQuicConfig(&quic.Config{
    +				MaxIdleTimeout: 10 * time.Second,
    +				Tracer:         newTracer(&eventRecorder),
    +			})
    +		},
    +	)
    +
    +	req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("https://localhost:%d/headers", port), nil)
    +	require.NoError(t, err)
    +
    +	_, requestErr = cl.Do(req)
    +	require.Error(t, requestErr)
    +	require.True(t, handlerCalled.Load())
    +
    +	var found bool
    +	for _, ev := range eventRecorder.Events(qlog.FrameParsed{}) {
    +		fp := ev.(qlog.FrameParsed)
    +		if _, ok := fp.Frame.Frame.(qlog.HeadersFrame); ok {
    +			headersFrameSize = fp.Raw.PayloadLength
    +			found = true
    +			break
    +		}
    +	}
    +	require.True(t, found)
    +	return headersFrameSize, requestErr
    +}
    +
     func TestHTTPTrailers(t *testing.T) {
     	mux := http.NewServeMux()
     	mux.HandleFunc("/trailers", func(w http.ResponseWriter, r *http.Request) {
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.