From ccde9e701a1e1d3cee82412b188cfdff0339ccdb Mon Sep 17 00:00:00 2001 From: chaosinthecrd Date: Tue, 11 Aug 2026 13:08:19 +0000 Subject: [PATCH] k8s-operator/sessionrecording/spdy: don't pre-size header map from wire Signed-off-by: chaosinthecrd --- k8s-operator/sessionrecording/spdy/frame.go | 7 +++++- .../sessionrecording/spdy/frame_test.go | 25 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/k8s-operator/sessionrecording/spdy/frame.go b/k8s-operator/sessionrecording/spdy/frame.go index ae103c7e0..53a993317 100644 --- a/k8s-operator/sessionrecording/spdy/frame.go +++ b/k8s-operator/sessionrecording/spdy/frame.go @@ -222,7 +222,12 @@ func parseHeaders(decompressor io.Reader, log *zap.SugaredLogger) (http.Header, if err != nil { return nil, fmt.Errorf("error determining num headers: %v", err) } - h := make(http.Header, numHeaders) + // numHeaders is attacker controlled, so it must not be used to pre-size the + // map: a small frame can declare ~4 billion headers and make Go reserve + // gigabytes of bucket storage before a single header is read. The map grows + // itself fine, and the io.LimitReader above bounds how many we can actually + // read. + h := make(http.Header) for range numHeaders { name, err := readLenBytes() if err != nil { diff --git a/k8s-operator/sessionrecording/spdy/frame_test.go b/k8s-operator/sessionrecording/spdy/frame_test.go index 252666119..2eed0078e 100644 --- a/k8s-operator/sessionrecording/spdy/frame_test.go +++ b/k8s-operator/sessionrecording/spdy/frame_test.go @@ -261,6 +261,31 @@ func Test_spdyFrame_parseHeaders_decompressionBomb(t *testing.T) { if got.Get("x") != strings.Repeat("A", 16) { t.Fatalf("unexpected header value: got %q", got.Get("x")) } + + // A block declaring a huge number of headers must be rejected without + // pre-sizing a map for that many entries. The declared count is only 4 + // bytes, so it slips under the io.LimitReader cap; the fix is to not feed + // it to make(). Without that fix this does not fail, it OOMs the binary. + countBomb := bytes.NewBuffer(nil) + writeControlFramePayloadBeforeHeaders(t, countBomb, SYN_STREAM, 1) + cw, err := zlib.NewWriterLevelDict(countBomb, zlib.BestCompression, spdyTxtDictionary) + if err != nil { + t.Fatalf("error creating zlib writer: %v", err) + } + if err := binary.Write(cw, binary.BigEndian, uint32(0xFFFFFFFF)); err != nil { + t.Fatal(err) + } + if err := cw.Flush(); err != nil { + t.Fatal(err) + } + if err := cw.Close(); err != nil { + t.Fatal(err) + } + var z3 zlibReader + sf3 := &spdyFrame{Ctrl: true, Type: SYN_STREAM, Payload: countBomb.Bytes()} + if _, err := sf3.parseHeaders(&z3, zl.Sugar()); err == nil { + t.Fatal("parseHeaders accepted a block declaring 0xFFFFFFFF headers") + } } // Test_spdyFrame_ParseRand calls spdyFrame.Parse with randomly generated bytes