Skip to content

Commit 1e63c2f

Browse files
committed
http2: limit canonical header cache by bytes, not entries
The canonical header cache is a per-connection cache mapping header keys to their canonicalized form. (For example, "foo-bar" => "Foo-Bar"). We limit the number of entries in the cache to prevent an attacker from consuming unbounded amounts of memory by sending many unique keys, but a small number of very large keys can still consume an unreasonable amount of memory. Track the amount of memory consumed by the cache and limit it based on memory rather than number of entries. Thanks to Josselin Costanzi for reporting this issue. For golang/go#56350 Change-Id: I41db4c9823ed5bf371a9881accddff1268489b16 Reviewed-on: https://go-review.googlesource.com/c/net/+/455635 Reviewed-by: Jenny Rakoczy <[email protected]> Run-TryBot: Damien Neil <[email protected]> TryBot-Result: Gopher Robot <[email protected]>
1 parent 3247b5b commit 1e63c2f

File tree

2 files changed

+36
-7
lines changed

2 files changed

+36
-7
lines changed

http2/server.go

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,7 @@ type serverConn struct {
588588
maxFrameSize int32
589589
peerMaxHeaderListSize uint32 // zero means unknown (default)
590590
canonHeader map[string]string // http2-lower-case -> Go-Canonical-Case
591+
canonHeaderKeysSize int // canonHeader keys size in bytes
591592
writingFrame bool // started writing a frame (on serve goroutine or separate)
592593
writingFrameAsync bool // started a frame on its own goroutine but haven't heard back on wroteFrameCh
593594
needsFrameFlush bool // last frame write wasn't a flush
@@ -766,6 +767,13 @@ func (sc *serverConn) condlogf(err error, format string, args ...interface{}) {
766767
}
767768
}
768769

770+
// maxCachedCanonicalHeadersKeysSize is an arbitrarily-chosen limit on the size
771+
// of the entries in the canonHeader cache.
772+
// This should be larger than the size of unique, uncommon header keys likely to
773+
// be sent by the peer, while not so high as to permit unreasonable memory usage
774+
// if the peer sends an unbounded number of unique header keys.
775+
const maxCachedCanonicalHeadersKeysSize = 2048
776+
769777
func (sc *serverConn) canonicalHeader(v string) string {
770778
sc.serveG.check()
771779
buildCommonHeaderMapsOnce()
@@ -781,14 +789,10 @@ func (sc *serverConn) canonicalHeader(v string) string {
781789
sc.canonHeader = make(map[string]string)
782790
}
783791
cv = http.CanonicalHeaderKey(v)
784-
// maxCachedCanonicalHeaders is an arbitrarily-chosen limit on the number of
785-
// entries in the canonHeader cache. This should be larger than the number
786-
// of unique, uncommon header keys likely to be sent by the peer, while not
787-
// so high as to permit unreasonable memory usage if the peer sends an unbounded
788-
// number of unique header keys.
789-
const maxCachedCanonicalHeaders = 32
790-
if len(sc.canonHeader) < maxCachedCanonicalHeaders {
792+
size := 100 + len(v)*2 // 100 bytes of map overhead + key + value
793+
if sc.canonHeaderKeysSize+size <= maxCachedCanonicalHeadersKeysSize {
791794
sc.canonHeader[v] = cv
795+
sc.canonHeaderKeysSize += size
792796
}
793797
return cv
794798
}

http2/server_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4543,3 +4543,28 @@ func TestServerInitialFlowControlWindow(t *testing.T) {
45434543
})
45444544
}
45454545
}
4546+
4547+
// TestCanonicalHeaderCacheGrowth verifies that the canonical header cache
4548+
// size is capped to a reasonable level.
4549+
func TestCanonicalHeaderCacheGrowth(t *testing.T) {
4550+
defer disableGoroutineTracking()()
4551+
for _, size := range []int{1, (1 << 20) - 10} {
4552+
base := strings.Repeat("X", size)
4553+
sc := &serverConn{}
4554+
const count = 1000
4555+
for i := 0; i < count; i++ {
4556+
h := fmt.Sprintf("%v-%v", base, i)
4557+
c := sc.canonicalHeader(h)
4558+
if len(h) != len(c) {
4559+
t.Errorf("sc.canonicalHeader(%q) = %q, want same length", h, c)
4560+
}
4561+
}
4562+
total := 0
4563+
for k, v := range sc.canonHeader {
4564+
total += len(k) + len(v) + 100
4565+
}
4566+
if total > maxCachedCanonicalHeadersKeysSize {
4567+
t.Errorf("after adding %v ~%v-byte headers, canonHeader cache is ~%v bytes, want <%v", count, size, total, maxCachedCanonicalHeadersKeysSize)
4568+
}
4569+
}
4570+
}

0 commit comments

Comments
 (0)