diff --git a/caveat_set.go b/caveat_set.go index 0c846ab..0297d8d 100644 --- a/caveat_set.go +++ b/caveat_set.go @@ -130,8 +130,9 @@ func (c *CaveatSet) DecodeMsgpack(dec *msgpack.Decoder) error { nCavs := aLen / 2 + // aLen is attacker-controlled, so it must not size the allocation. if c.Caveats == nil { - c.Caveats = make([]Caveat, 0, nCavs) + c.Caveats = make([]Caveat, 0) } for i := 0; i < nCavs; i++ { diff --git a/caveat_set_decode_test.go b/caveat_set_decode_test.go new file mode 100644 index 0000000..2347ce4 --- /dev/null +++ b/caveat_set_decode_test.go @@ -0,0 +1,38 @@ +package macaroon + +import ( + "bytes" + "runtime" + "testing" + + "github.com/alecthomas/assert/v2" + "github.com/vmihailenco/msgpack/v5" +) + +// The caveat container's array length comes from the wire and must not +// drive memory reservation: a header that promises millions of caveats +// backed by no data has to fail cheaply. +func TestDecodeCaveatSetOversizedHeader(t *testing.T) { + const claimedCaveats = 1 << 21 + + var buf bytes.Buffer + assert.NoError(t, msgpack.NewEncoder(&buf).EncodeArrayLen(claimedCaveats*2)) + header := buf.Bytes() + + var before, after runtime.MemStats + runtime.GC() + runtime.ReadMemStats(&before) + + var cs CaveatSet + err := msgpack.NewDecoder(bytes.NewReader(header)).Decode(&cs) + + runtime.ReadMemStats(&after) + + assert.Error(t, err) + assert.Equal(t, 0, len(cs.Caveats)) + + allocated := after.TotalAlloc - before.TotalAlloc + if allocated > 1<<20 { + t.Fatalf("decoding a %d-byte header allocated %d bytes", len(header), allocated) + } +}