CVE-2023-37475: Uncontrolled Resource Consumption (CWE-400)
value: Hamba avro is a go lang encoder/decoder implementation of the avro codec specification. In affected versions a well-crafted string passed to avro's `github.com/hamba/avro/v2.Unmarshal()` can throw a `fatal error: runtime: out of memory` which is unrecoverable and can cause denial of service of the consumer of avro. The root cause of the issue is that avro uses part of the input to `Unmarshal()` to determine the size when creating a new slice and hence an attacker may consume arbitrary amounts of memory which in turn may cause the application to crash. This issue has been addressed in commit `b4a402f4` which has been included in release version `2.13.0`. Users are advised to upgrade. There are no known workarounds for this vulnerability.
CWE description: The product does not properly control the allocation and maintenance of a limited resource, thereby enabling an actor to influence the amount of resources consumed, eventually leading to the exhaustion of available resources.
CVSS v3 base score: 7.5
Published: 2023-07-17T17:15Z
Repository: https://github.com/hamba/avro
Language: Go
Files changed: README.md, config.go, reader.go, reader_test.go
Commit date: 2023-07-16 19:22:09 +0200
Commit message: feat: add max byte slice size config (#273)


Diff: diff --git a/README.md b/README.md
index 433683d..34ba5ec 100644
--- a/README.md
+++ b/README.md
@@ -123,15 +123,22 @@ be tested first for implementation of these interfaces, in the case of a `string
 encoding and decoding.
 
 Enums may also implement `TextMarshaler` and `TextUnmarshaler`, and must resolve to valid symbols in the given enum schema.
 
 ##### Identical Underlying Types
+
 One type can be [ConvertibleTo](https://go.dev/ref/spec#Conversions) another type if they have identical underlying types. 
 A non-native type is allowed be used if it can be convertible to *time.Time*, *big.Rat* or *avro.LogicalDuration* for the particular of *LogicalTypes*.
 
 Ex.: `type Timestamp time.Time`
 
+##### Untrusted Input With Bytes and Strings
+
+For security reasons, the configuration `Config.MaxByteSliceSize` restricts the maximum size of `bytes` and `string` types created
+by the `Reader`. The default maximum size is `1MiB` and is configurable. This is required to stop untrusted input from consuming all memory and
+crashing the application. Should this not be need, setting a negative number will disable the behaviour.
+
 ### Recursive Structs
 
 At this moment recursive structs are not supported. It is planned for the future.
 
 ## Benchmark
diff --git a/config.go b/config.go
index b03a4c6..ad299a7 100644
--- a/config.go
+++ b/config.go
@@ -6,10 +6,12 @@ import (
 	"sync"
 
 	"github.com/modern-go/reflect2"
 )
 
+const maxByteSliceSize = 1024 * 1024
+
 // DefaultConfig is the default API.
 var DefaultConfig = Config{}.Freeze()
 
 // Config customises how the codec should behave.
 type Config struct {
@@ -41,10 +43,14 @@ type Config struct {
 	PartialUnionTypeResolution bool
 
 	// Disable caching layer for encoders and decoders, forcing them to get rebuilt on every
 	// call to Marshal() and Unmarshal()
 	DisableCaching bool
+
+	// MaxByteSliceSize is the maximum size of `bytes` or `string` types the Reader will create, defaulting to 1MiB.
+	// If this size is exceeded, the Reader returns an error. This can be disabled by setting a negative number.
+	MaxByteSliceSize int
 }
 
 // Freeze makes the configuration immutable.
 func (c Config) Freeze() API {
 	api := &frozenConfig{
@@ -250,5 +256,13 @@ func (c *frozenConfig) getBlockLength() int {
 	if blockSize <= 0 {
 		return 100
 	}
 	return blockSize
 }
+
+func (c *frozenConfig) getMaxByteSliceSize() int {
+	size := c.config.MaxByteSliceSize
+	if size == 0 {
+		return maxByteSliceSize
+	}
+	return size
+}
diff --git a/reader.go b/reader.go
index a5daabd..96b8075 100644
--- a/reader.go
+++ b/reader.go
@@ -2,10 +2,11 @@ package avro
 
 import (
 	"errors"
 	"fmt"
 	"io"
+	"strings"
 	"unsafe"
 )
 
 const (
 	maxIntBufSize  = 5
@@ -214,16 +215,22 @@ func (r *Reader) ReadString() string {
 }
 
 func (r *Reader) readBytes(op string) []byte {
 	size := int(r.ReadLong())
 	if size < 0 {
-		r.ReportError("ReadString", "invalid "+op+" length")
+		fnName := "Read" + strings.ToTitle(op)
+		r.ReportError(fnName, "invalid "+op+" length")
 		return nil
 	}
 	if size == 0 {
 		return []byte{}
 	}
+	if max := r.cfg.getMaxByteSliceSize(); max > 0 && size > max {
+		fnName := "Read" + strings.ToTitle(op)
+		r.ReportError(fnName, "size is greater than `Config.MaxByteSliceSize`")
+		return nil
+	}
 
 	// The bytes are entirely in the buffer and of a reasonable size.
 	// Use the byte slab.
 	if r.head+size <= r.tail && size <= 1024 {
 		if cap(r.slab) < size {
diff --git a/reader_test.go b/reader_test.go
index 5488493..f0835f2 100644
--- a/reader_test.go
+++ b/reader_test.go
@@ -505,10 +505,23 @@ func TestReader_ReadBytes(t *testing.T) {
 			assert.Equal(t, test.want, got)
 		})
 	}
 }
 
+func TestReader_ReadBytesLargerThanMaxByteSliceSize(t *testing.T) {
+	data := []byte{
+		246, 255, 255, 255, 255, 10, 255, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
+		32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
+		32, 32, 32, 32, 32, 32, 32,
+	}
+	r := avro.NewReader(bytes.NewReader(data), 4)
+
+	_ = r.ReadBytes()
+
+	assert.Error(t, r.Error)
+}
+
 func TestReader_ReadString(t *testing.T) {
 	tests := []struct {
 		data    []byte
 		want    string
 		wantErr require.ErrorAssertionFunc
@@ -581,10 +594,23 @@ func TestReader_ReadString(t *testing.T) {
 			assert.Equal(t, test.want, got)
 		})
 	}
 }
 
+func TestReader_ReadStringLargerThanMaxByteSliceSize(t *testing.T) {
+	data := []byte{
+		246, 255, 255, 255, 255, 10, 255, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
+		32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
+		32, 32, 32, 32, 32, 32, 32,
+	}
+	r := avro.NewReader(bytes.NewReader(data), 4)
+
+	_ = r.ReadString()
+
+	assert.Error(t, r.Error)
+}
+
 func TestReader_ReadStringFastPathIsntBoundToBuffer(t *testing.T) {
 	data := []byte{0x06, 0x66, 0x6F, 0x6F, 0x08, 0x61, 0x76, 0x72, 0x6F}
 	r := avro.NewReader(bytes.NewReader(data), 4)
 
 	got1 := r.ReadString()

Vulnerable code: 		r.ReportError("ReadString", "invalid "+op+" length")
Fixed code: 
##### Untrusted Input With Bytes and Strings

For security reasons, the configuration `Config.MaxByteSliceSize` restricts the maximum size of `bytes` and `string` types created
by the `Reader`. The default maximum size is `1MiB` and is configurable. This is required to stop untrusted input from consuming all memory and
crashing the application. Should this not be need, setting a negative number will disable the behaviour.

const maxByteSliceSize = 1024 * 1024


	// MaxByteSliceSize is the maximum size of `bytes` or `string` types the Reader will create, defaulting to 1MiB.
	// If this size is exceeded, the Reader returns an error. This can be disabled by setting a negative number.
	MaxByteSliceSize int

func (c *frozenConfig) getMaxByteSliceSize() int {
	size := c.config.MaxByteSliceSize
	if size == 0 {
		return maxByteSliceSize
	}
	return size
}
	"strings"
		fnName := "Read" + strings.ToTitle(op)
		r.ReportError(fnName, "invalid "+op+" length")
	if max := r.cfg.getMaxByteSliceSize(); max > 0 && size > max {
		fnName := "Read" + strings.ToTitle(op)
		r.ReportError(fnName, "size is greater than `Config.MaxByteSliceSize`")
		return nil
	}
func TestReader_ReadBytesLargerThanMaxByteSliceSize(t *testing.T) {
	data := []byte{
		246, 255, 255, 255, 255, 10, 255, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
		32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
		32, 32, 32, 32, 32, 32, 32,
	}
	r := avro.NewReader(bytes.NewReader(data), 4)

	_ = r.ReadBytes()

	assert.Error(t, r.Error)
}

func TestReader_ReadStringLargerThanMaxByteSliceSize(t *testing.T) {
	data := []byte{
		246, 255, 255, 255, 255, 10, 255, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
		32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
		32, 32, 32, 32, 32, 32, 32,
	}
	r := avro.NewReader(bytes.NewReader(data), 4)

	_ = r.ReadString()

	assert.Error(t, r.Error)
}