Browse Source

http2: advertise and enforce hpack max header list size

Thanks to Andy Bursavich for the example attack.
Pair programmed with Dmitri Shuralyov.

Fixes golang/go#12843

Change-Id: Ic412c9364b37a10e5164232aa809b956874fae08
Reviewed-on: https://go-review.googlesource.com/15751
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
Brad Fitzpatrick 10 years ago
parent
commit
29704b8f8f
4 changed files with 162 additions and 9 deletions
  1. 39 7
      http2/hpack/hpack.go
  2. 38 0
      http2/hpack/hpack_test.go
  3. 16 2
      http2/server.go
  4. 69 0
      http2/server_test.go

+ 39 - 7
http2/hpack/hpack.go

@@ -64,6 +64,10 @@ type Decoder struct {
 	dynTab dynamicTable
 	emit   func(f HeaderField)
 
+	headerListSize    int64
+	maxHeaderListSize uint32 // 0 means unlimited
+	hitLimit          bool
+
 	// buf is the unparsed buffer. It's only written to
 	// saveBuf if it was truncated in the middle of a header
 	// block. Because it's usually not owned, we can only
@@ -72,15 +76,26 @@ type Decoder struct {
 	saveBuf bytes.Buffer
 }
 
-func NewDecoder(maxSize uint32, emitFunc func(f HeaderField)) *Decoder {
+// NewDecoder returns a new decoder with the provided maximum dynamic
+// table size. The emitFunc will be called for each valid field
+// parsed.
+func NewDecoder(maxDynamicTableSize uint32, emitFunc func(f HeaderField)) *Decoder {
 	d := &Decoder{
 		emit: emitFunc,
 	}
-	d.dynTab.allowedMaxSize = maxSize
-	d.dynTab.setMaxSize(maxSize)
+	d.dynTab.allowedMaxSize = maxDynamicTableSize
+	d.dynTab.setMaxSize(maxDynamicTableSize)
 	return d
 }
 
+// SetMaxHeaderListSize sets the decoder's SETTINGS_MAX_HEADER_LIST_SIZE.
+// It should be set before any call to Write.
+// The default, 0, means unlimited.
+// If the limit is passed, calls to Write and Close will return ErrMaxHeaderListSize.
+func (d *Decoder) SetMaxHeaderListSize(v uint32) {
+	d.maxHeaderListSize = v
+}
+
 // TODO: add method *Decoder.Reset(maxSize, emitFunc) to let callers re-use Decoders and their
 // underlying buffers for garbage reasons.
 
@@ -220,11 +235,16 @@ func (d *Decoder) DecodeFull(p []byte) ([]HeaderField, error) {
 	return hf, nil
 }
 
+var ErrMaxHeaderListSize = errors.New("hpack: max header list size exceeded")
+
 func (d *Decoder) Close() error {
 	if d.saveBuf.Len() > 0 {
 		d.saveBuf.Reset()
 		return DecodingError{errors.New("truncated headers")}
 	}
+	if d.hitLimit {
+		return ErrMaxHeaderListSize
+	}
 	return nil
 }
 
@@ -245,7 +265,7 @@ func (d *Decoder) Write(p []byte) (n int, err error) {
 		d.saveBuf.Reset()
 	}
 
-	for len(d.buf) > 0 {
+	for len(d.buf) > 0 && !d.hitLimit {
 		err = d.parseHeaderFieldRepr()
 		if err != nil {
 			if err == errNeedMore {
@@ -255,7 +275,9 @@ func (d *Decoder) Write(p []byte) (n int, err error) {
 			break
 		}
 	}
-
+	if err == nil && d.hitLimit {
+		err = ErrMaxHeaderListSize
+	}
 	return len(p), err
 }
 
@@ -323,7 +345,7 @@ func (d *Decoder) parseFieldIndexed() error {
 	if !ok {
 		return DecodingError{InvalidIndexError(idx)}
 	}
-	d.emit(HeaderField{Name: hf.Name, Value: hf.Value})
+	d.callEmit(HeaderField{Name: hf.Name, Value: hf.Value})
 	d.buf = buf
 	return nil
 }
@@ -358,10 +380,20 @@ func (d *Decoder) parseFieldLiteral(n uint8, it indexType) error {
 		d.dynTab.add(hf)
 	}
 	hf.Sensitive = it.sensitive()
-	d.emit(hf)
+	d.callEmit(hf)
 	return nil
 }
 
+func (d *Decoder) callEmit(hf HeaderField) {
+	const overheadPerField = 32 // per http2 section 6.5.2, etc
+	d.headerListSize += int64(len(hf.Name)+len(hf.Value)) + overheadPerField
+	if d.maxHeaderListSize != 0 && d.headerListSize > int64(d.maxHeaderListSize) {
+		d.hitLimit = true
+		return
+	}
+	d.emit(hf)
+}
+
 // (same invariants and behavior as parseHeaderFieldRepr)
 func (d *Decoder) parseDynamicTableSizeUpdate() error {
 	buf := d.buf

+ 38 - 0
http2/hpack/hpack_test.go

@@ -646,3 +646,41 @@ func dehex(s string) []byte {
 	}
 	return b
 }
+
+func TestMaxHeaderListSize(t *testing.T) {
+	tests := []struct {
+		fields  []HeaderField
+		max     int
+		wantErr bool
+	}{
+		// Plenty of space.
+		{
+			fields: []HeaderField{{Name: "foo", Value: "bar"}},
+			max:    500,
+		},
+		// Exactly right limit.
+		{
+			fields: []HeaderField{{Name: "foo", Value: "bar"}},
+			max:    len("foo") + len("bar") + 32,
+		},
+		// One byte too short.
+		{
+			fields:  []HeaderField{{Name: "foo", Value: "bar"}},
+			max:     len("foo") + len("bar") + 32 - 1,
+			wantErr: true,
+		},
+	}
+	for i, tt := range tests {
+		var buf bytes.Buffer
+		enc := NewEncoder(&buf)
+		for _, hf := range tt.fields {
+			enc.WriteField(hf)
+		}
+		dec := NewDecoder(8<<20, func(HeaderField) {})
+		dec.SetMaxHeaderListSize(uint32(tt.max))
+		_, err := dec.Write(buf.Bytes())
+		if (err != nil) != tt.wantErr {
+			t.Errorf("%d. err = %v; want err = %v", i, err, tt.wantErr)
+		}
+	}
+}

+ 16 - 2
http2/server.go

@@ -220,6 +220,7 @@ func (srv *Server) handleConn(hs *http.Server, c net.Conn, h http.Handler) {
 	sc.inflow.add(initialWindowSize)
 	sc.hpackEncoder = hpack.NewEncoder(&sc.headerWriteBuf)
 	sc.hpackDecoder = hpack.NewDecoder(initialHeaderTableSize, sc.onNewHeaderField)
+	sc.hpackDecoder.SetMaxHeaderListSize(sc.maxHeaderListSize())
 
 	fr := NewFramer(sc.bw, c)
 	fr.SetMaxReadFrameSize(srv.maxReadFrameSize())
@@ -353,7 +354,7 @@ type serverConn struct {
 	streams               map[uint32]*stream
 	initialWindowSize     int32
 	headerTableSize       uint32
-	maxHeaderListSize     uint32            // zero means unknown (default)
+	peerMaxHeaderListSize uint32            // zero means unknown (default)
 	canonHeader           map[string]string // http2-lower-case -> Go-Canonical-Case
 	req                   requestParam      // non-zero while reading request headers
 	writingFrame          bool              // started write goroutine but haven't heard back on wroteFrameCh
@@ -370,6 +371,18 @@ type serverConn struct {
 	hpackEncoder   *hpack.Encoder
 }
 
+func (sc *serverConn) maxHeaderListSize() uint32 {
+	n := sc.hs.MaxHeaderBytes
+	if n == 0 {
+		n = http.DefaultMaxHeaderBytes
+	}
+	// http2's count is in a slightly different unit and includes 32 bytes per pair.
+	// So, take the net/http.Server value and pad it up a bit, assuming 10 headers.
+	const perFieldOverhead = 32 // per http2 spec
+	const typicalHeaders = 10   // conservative
+	return uint32(n + typicalHeaders*perFieldOverhead)
+}
+
 // requestParam is the state of the next request, initialized over
 // potentially several frames HEADERS + zero or more CONTINUATION
 // frames.
@@ -602,6 +615,7 @@ func (sc *serverConn) serve() {
 		write: writeSettings{
 			{SettingMaxFrameSize, sc.srv.maxReadFrameSize()},
 			{SettingMaxConcurrentStreams, sc.advMaxStreams},
+			{SettingMaxHeaderListSize, sc.maxHeaderListSize()},
 
 			// TODO: more actual settings, notably
 			// SettingInitialWindowSize, but then we also
@@ -1100,7 +1114,7 @@ func (sc *serverConn) processSetting(s Setting) error {
 	case SettingMaxFrameSize:
 		sc.writeSched.maxFrameSize = s.Val
 	case SettingMaxHeaderListSize:
-		sc.maxHeaderListSize = s.Val
+		sc.peerMaxHeaderListSize = s.Val
 	default:
 		// Unknown setting: "An endpoint that receives a SETTINGS
 		// frame with any unknown or unsupported identifier MUST

+ 69 - 0
http2/server_test.go

@@ -2188,6 +2188,75 @@ func testServerWithCurl(t *testing.T, permitProhibitedCipherSuites bool) {
 	}
 }
 
+// Issue 12843
+func TestServerDoS_MaxHeaderListSize(t *testing.T) {
+	st := newServerTester(t, func(w http.ResponseWriter, r *http.Request) {})
+	defer st.Close()
+
+	// shake hands
+	st.writePreface()
+	st.writeInitialSettings()
+	frameSize := defaultMaxReadFrameSize
+	var advHeaderListSize *uint32
+	st.wantSettings().ForeachSetting(func(s Setting) error {
+		switch s.ID {
+		case SettingMaxFrameSize:
+			if s.Val < minMaxFrameSize {
+				frameSize = minMaxFrameSize
+			} else if s.Val > maxFrameSize {
+				frameSize = maxFrameSize
+			} else {
+				frameSize = int(s.Val)
+			}
+		case SettingMaxHeaderListSize:
+			advHeaderListSize = &s.Val
+		}
+		return nil
+	})
+	st.writeSettingsAck()
+	st.wantSettingsAck()
+
+	if advHeaderListSize == nil {
+		t.Errorf("server didn't advertise a max header list size")
+	} else if *advHeaderListSize == 0 {
+		t.Errorf("server advertised a max header list size of 0")
+	}
+
+	st.encodeHeaderField(":method", "GET")
+	st.encodeHeaderField(":path", "/")
+	st.encodeHeaderField(":scheme", "https")
+	cookie := strings.Repeat("*", 4058)
+	st.encodeHeaderField("cookie", cookie)
+	st.writeHeaders(HeadersFrameParam{
+		StreamID:      1,
+		BlockFragment: st.headerBuf.Bytes(),
+		EndStream:     true,
+		EndHeaders:    false,
+	})
+
+	// Capture the short encoding of a duplicate ~4K cookie, now
+	// that we've already sent it once.
+	st.headerBuf.Reset()
+	st.encodeHeaderField("cookie", cookie)
+
+	// Now send 1MB of it.
+	const size = 1 << 20
+	b := bytes.Repeat(st.headerBuf.Bytes(), size/st.headerBuf.Len())
+	for len(b) > 0 {
+		chunk := b
+		if len(chunk) > frameSize {
+			chunk = chunk[:frameSize]
+		}
+		b = b[len(chunk):]
+		st.fr.WriteContinuation(1, len(b) == 0, chunk)
+	}
+
+	fr, err := st.fr.ReadFrame()
+	if err == nil {
+		t.Fatalf("want error; got unexpected frame: %#v", fr)
+	}
+}
+
 func BenchmarkServerGets(b *testing.B) {
 	b.ReportAllocs()