diff --git a/x/mongo/driver/compression.go b/x/mongo/driver/compression.go index 349781f4e..690d3e10f 100644 --- a/x/mongo/driver/compression.go +++ b/x/mongo/driver/compression.go @@ -153,8 +153,23 @@ var zstdReaderPool = sync.Pool{ }, } +// maxDecompressedSize is the largest UncompressedSize that DecompressPayload +// will honor. It matches the MongoDB wire protocol maximum message size +// (defaultMaxMessageSize in x/mongo/driver/topology/connection.go, 48000000 +// bytes / ~48MB): a legitimate uncompressed message can never exceed it. +const maxDecompressedSize = 48000000 + // DecompressPayload takes a byte slice that has been compressed and undoes it according to the options passed func DecompressPayload(in []byte, opts CompressionOpts) ([]byte, error) { + // UncompressedSize is read directly off the wire (see + // wiremessage.ReadCompressedUncompressedSize) and is used below as an + // allocation size. Validate it before allocating so a malformed or + // malicious OP_COMPRESSED frame cannot drive an unbounded allocation + // (out of memory) or a make([]byte, negative) panic. + if opts.UncompressedSize < 0 || opts.UncompressedSize > maxDecompressedSize { + return nil, fmt.Errorf("invalid uncompressed size %d: must be between 0 and %d", opts.UncompressedSize, maxDecompressedSize) + } + switch opts.Compressor { case wiremessage.CompressorNoOp: return in, nil diff --git a/x/mongo/driver/compression_test.go b/x/mongo/driver/compression_test.go index 10c35eb0e..c716df0bb 100644 --- a/x/mongo/driver/compression_test.go +++ b/x/mongo/driver/compression_test.go @@ -102,6 +102,41 @@ func TestDecompressFailures(t *testing.T) { _, err = DecompressPayload(compressedData, opts) assert.Error(t, err) }) + + // UncompressedSize is attacker-controlled (read straight off the wire in + // Operation.decompressWireMessage). An out-of-range value must be rejected + // with an error before it is used as an allocation size, otherwise a + // malicious server can drive an unbounded allocation (OOM) or a + // make([]byte, negative) panic. + validZlibEmptyBody := []byte{0x78, 0x9c, 0x03, 0x00, 0x00, 0x00, 0x00, 0x01} + + oversized := []struct { + name string + compressor wiremessage.CompressorID + in []byte + size int32 + }{ + {"zlib oversized size", wiremessage.CompressorZLib, validZlibEmptyBody, 2147483647}, + {"zlib negative size", wiremessage.CompressorZLib, validZlibEmptyBody, -1}, + {"zstd oversized size", wiremessage.CompressorZstd, []byte{}, 2147483647}, + {"zstd negative size", wiremessage.CompressorZstd, []byte{}, -1}, + {"snappy oversized size", wiremessage.CompressorSnappy, []byte{}, 2147483647}, + {"snappy negative size", wiremessage.CompressorSnappy, []byte{}, -1}, + } + for _, tc := range oversized { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + opts := CompressionOpts{ + Compressor: tc.compressor, + UncompressedSize: tc.size, + } + // Must return an error rather than allocating tc.size bytes or panicking. + _, err := DecompressPayload(tc.in, opts) + assert.Error(t, err) + }) + } } var (