mirror of
https://github.com/tailscale/tailscale.git
synced 2026-09-23 12:05:15 -04:00
k8s-operator/sessionrecording/spdy: bound decompressed header size
parseHeaders reads attacker-controlled field lengths off the zlib decompressor with no ceiling on the decompressed output, so a small SPDY control frame can inflate ~1000x and exhaust memory. Cap the block with io.LimitReader. Updates #20510 Signed-off-by: basavaraj-sm05 <basavaraj@digiscrypt.com>
This commit is contained in:
1 parent
82a381e54b
commit
2f39c8f526
2 files changed
+71
No files matched your search
@@ -22,6 +22,10 @@
|
||||
SYN_PING ControlFrameType = 6 // https://www.ietf.org/archive/id/draft-mbelshe-httpbis-spdy-00.txt section 2.6.5
|
||||
)
|
||||
|
||||
// maxHeaderBlockLength bounds the total decompressed size of a single SPDY
|
||||
// Name/Value header block that parseHeaders will read from an untrusted peer.
|
||||
const maxHeaderBlockLength = 1 << 20 // 1 MiB
|
||||
|
||||
// spdyFrame is a parsed SPDY frame as defined in
|
||||
// https://www.ietf.org/archive/id/draft-mbelshe-httpbis-spdy-00.txt
|
||||
// A SPDY frame can be either a control frame or a data frame.
|
||||
@@ -178,6 +182,14 @@ func (sf *spdyFrame) parseHeaders(z *zlibReader, log *zap.SugaredLogger) (http.H
|
||||
// also advances the provided reader past the headers block.
|
||||
// See also https://www.ietf.org/archive/id/draft-mbelshe-httpbis-spdy-00.txt section 2.6.10
|
||||
func parseHeaders(decompressor io.Reader, log *zap.SugaredLogger) (http.Header, error) {
|
||||
// The Name/Value block is zlib compressed and its declared field lengths
|
||||
// are attacker controlled. zlib can expand ~1000x, so without a ceiling a
|
||||
// peer can turn a small control frame into gigabytes of decompressed data
|
||||
// (the field lengths below feed io.CopyN and a uint32->int conversion). Cap
|
||||
// the decompressed bytes we're willing to read for a single header block.
|
||||
// Real 'kubectl exec/attach' blocks are a few hundred bytes; the websocket
|
||||
// recorder applies an equivalent payload cap in ../ws/message.go.
|
||||
decompressor = io.LimitReader(decompressor, maxHeaderBlockLength)
|
||||
buf := bufPool.Get().(*bytes.Buffer)
|
||||
defer bufPool.Put(buf)
|
||||
buf.Reset()
|
||||
|
||||
@@ -204,6 +204,65 @@ func Test_spdyFrame_parseHeaders(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Test_spdyFrame_parseHeaders_decompressionBomb verifies that a control frame
|
||||
// whose zlib-compressed Name/Value block decompresses to more than
|
||||
// maxHeaderBlockLength is rejected rather than expanded into memory. A tiny
|
||||
// frame can otherwise declare a multi-GB value that zlib inflates ~1000x.
|
||||
func Test_spdyFrame_parseHeaders_decompressionBomb(t *testing.T) {
|
||||
zl, err := zap.NewDevelopment()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// buildBlock builds a SYN_STREAM payload with a single header whose value
|
||||
// is a valueLen-byte run of one byte (compresses tiny, decompresses large).
|
||||
buildBlock := func(valueLen int) []byte {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
writeControlFramePayloadBeforeHeaders(t, buf, SYN_STREAM, 1)
|
||||
w, err := zlib.NewWriterLevelDict(buf, zlib.BestCompression, spdyTxtDictionary)
|
||||
if err != nil {
|
||||
t.Fatalf("error creating zlib writer: %v", err)
|
||||
}
|
||||
if err := binary.Write(w, binary.BigEndian, uint32(1)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := binary.Write(w, binary.BigEndian, uint32(1)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := w.Write([]byte("x")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := binary.Write(w, binary.BigEndian, uint32(valueLen)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := w.Write(bytes.Repeat([]byte("A"), valueLen)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
w.Flush()
|
||||
w.Close()
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
oversized := buildBlock(maxHeaderBlockLength + 1)
|
||||
var z zlibReader
|
||||
sf := &spdyFrame{Ctrl: true, Type: SYN_STREAM, Payload: oversized}
|
||||
if _, err := sf.parseHeaders(&z, zl.Sugar()); err == nil {
|
||||
t.Fatalf("parseHeaders accepted a header block that decompresses past the %d byte cap", maxHeaderBlockLength)
|
||||
}
|
||||
|
||||
// A block that stays under the cap must still parse.
|
||||
small := buildBlock(16)
|
||||
var z2 zlibReader
|
||||
sf2 := &spdyFrame{Ctrl: true, Type: SYN_STREAM, Payload: small}
|
||||
got, err := sf2.parseHeaders(&z2, zl.Sugar())
|
||||
if err != nil {
|
||||
t.Fatalf("parseHeaders rejected a valid header block: %v", err)
|
||||
}
|
||||
if got.Get("x") != strings.Repeat("A", 16) {
|
||||
t.Fatalf("unexpected header value: got %q", got.Get("x"))
|
||||
}
|
||||
}
|
||||
|
||||
// Test_spdyFrame_ParseRand calls spdyFrame.Parse with randomly generated bytes
|
||||
// to test that it doesn't panic.
|
||||
func Test_spdyFrame_ParseRand(t *testing.T) {
|
||||
|
||||
Reference in new issue
Block a user