Skip to content

Commit

Permalink
Limit nested levels for arrays, maps, tags
Browse files Browse the repository at this point in the history
Decoder rejects nested level > 32 to prevent stack depth exhaustion attack.

Closes: #72
  • Loading branch information
fxamacker committed Dec 17, 2019
1 parent d2d6a95 commit 3aa4328
Show file tree
Hide file tree
Showing 2 changed files with 158 additions and 33 deletions.
89 changes: 89 additions & 0 deletions maxdepth_test.go
@@ -0,0 +1,89 @@
package cbor

import (
"encoding/hex"
"testing"
)

func TestDepth(t *testing.T) {
testCases := []struct {
name string
cborData []byte
wantDepth int
}{
{"uint", hexDecode("00"), 1}, // 0
{"int", hexDecode("20"), 1}, // -1
{"bool", hexDecode("f4"), 1}, // false
{"nil", hexDecode("f6"), 1}, // nil
{"float", hexDecode("fa47c35000"), 1}, // 100000.0
{"byte string", hexDecode("40"), 1}, // []byte{}
{"indefinite length byte string", hexDecode("5f42010243030405ff"), 1}, // []byte{1, 2, 3, 4, 5}
{"text string", hexDecode("60"), 1}, // ""
{"indefinite length text string", hexDecode("7f657374726561646d696e67ff"), 1}, // "streaming"
{"empty array", hexDecode("80"), 1}, // []
{"indefinite length empty array", hexDecode("9fff"), 1}, // []
{"array", hexDecode("98190102030405060708090a0b0c0d0e0f101112131415161718181819"), 2}, // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25]
{"indefinite length array", hexDecode("9f0102030405060708090a0b0c0d0e0f101112131415161718181819ff"), 2}, // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25]
{"nested array", hexDecode("8301820203820405"), 3}, // [1,[2,3],[4,5]]
{"indefinite length nested array", hexDecode("83018202039f0405ff"), 3}, // [1,[2,3],[4,5]]
{"array and map", hexDecode("826161a161626163"), 3}, // [a", {"b": "c"}]
{"indefinite length array and map", hexDecode("826161bf61626163ff"), 3}, // [a", {"b": "c"}]
{"empty map", hexDecode("a0"), 1}, // {}
{"indefinite length empty map", hexDecode("bfff"), 1}, // {}
{"map", hexDecode("a201020304"), 2}, // {1:2, 3:4}
{"nested map", hexDecode("a26161016162820203"), 3}, // {"a": 1, "b": [2, 3]}
{"indefinite length nested map", hexDecode("bf61610161629f0203ffff"), 3}, // {"a": 1, "b": [2, 3]}
{"tag", hexDecode("c074323031332d30332d32315432303a30343a30305a"), 1}, // 0("2013-03-21T20:04:00Z")
{"tagged map", hexDecode("d864a26161016162820203"), 3}, // 100({"a": 1, "b": [2, 3]})
{"tagged map and array", hexDecode("d864a26161016162d865d866820203"), 4}, // 100({"a": 1, "b": 101(102([2, 3]))})
{"nested tag", hexDecode("d864d865d86674323031332d30332d32315432303a30343a30305a"), 3}, // 100(101(102("2013-03-21T20:04:00Z")))
{"32-level array", hexDecode("820181818181818181818181818181818181818181818181818181818181818101"), 32},
{"32-level indefinite length array", hexDecode("9f0181818181818181818181818181818181818181818181818181818181818101ff"), 32},
{"32-level map", hexDecode("a10181818181818181818181818181818181818181818181818181818181818101"), 32},
{"32-level indefinite length map", hexDecode("bf0181818181818181818181818181818181818181818181818181818181818101ff"), 32},
{"32-level tag", hexDecode("d864d864d864d864d864d864d864d864d864d864d864d864d864d864d864d864d864d864d864d864d864d864d864d864d864d864d864d864d864d864d864d86474323031332d30332d32315432303a30343a30305a"), 32}, // 100(100(...("2013-03-21T20:04:00Z")))
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
_, depth, err := valid(tc.cborData, 0, 1)
if err != nil {
t.Errorf("valid(0x%x) returns error %s", tc.cborData, err)
}
if depth != tc.wantDepth {
t.Errorf("valid(0x%x) returns depth %d, want %d", tc.cborData, depth, tc.wantDepth)
}
})
}
}

func TestDepthError(t *testing.T) {
testCases := []struct {
name string
cborData []byte
wantErrorMsg string
}{
{"33-level array", hexDecode("82018181818181818181818181818181818181818181818181818181818181818101"), "cbor: reached max depth 32"},
{"33-level indefinite length array", hexDecode("9f018181818181818181818181818181818181818181818181818181818181818101ff"), "cbor: reached max depth 32"},
{"33-level map", hexDecode("a1018181818181818181818181818181818181818181818181818181818181818101"), "cbor: reached max depth 32"},
{"33-level indefinite length map", hexDecode("bf018181818181818181818181818181818181818181818181818181818181818101ff"), "cbor: reached max depth 32"},
{"33-level tag", hexDecode("d864d864d864d864d864d864d864d864d864d864d864d864d864d864d864d864d864d864d864d864d864d864d864d864d864d864d864d864d864d864d864d864d86474323031332d30332d32315432303a30343a30305a"), "cbor: reached max depth 32"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
_, _, err := valid(tc.cborData, 0, 1)
if err == nil {
t.Errorf("valid(0x%x) doesn't return error, want %s", tc.cborData, tc.wantErrorMsg)
} else if err.Error() != tc.wantErrorMsg {
t.Errorf("valid(0x%x) returns error %s, want %s", tc.cborData, err, tc.wantErrorMsg)
}
})
}
}

func hexDecode(s string) []byte {
data, err := hex.DecodeString(s)
if err != nil {
panic(err)
}
return data
}
102 changes: 69 additions & 33 deletions valid.go
Expand Up @@ -29,101 +29,137 @@ func Valid(data []byte) (rest []byte, err error) {
if len(data) == 0 {
return nil, io.EOF
}
offset, err := valid(data, 0)
offset, _, err := valid(data, 0, 1)
if err != nil {
return nil, err
}
return data[offset:], nil
}

func valid(data []byte, off int) (int, error) {
const (
maxNestingLevel = 32
)

func valid(data []byte, off int, depth int) (int, int, error) {
if depth > maxNestingLevel {
return 0, 0, errors.New("cbor: reached max depth " + strconv.Itoa(maxNestingLevel))
}
off, t, ai, val, err := validHead(data, off)
if err != nil {
return 0, err
return 0, 0, err
}
if ai == 31 {
return validIndefinite(data, off, t)
if t == cborTypeByteString || t == cborTypeTextString {
return validIndefiniteString(data, off, t, depth)
}
return validIndefiniteArrOrMap(data, off, t, depth)
}

switch t {
case cborTypeByteString, cborTypeTextString:
valInt := int(val)
if valInt < 0 {
// Detect integer overflow
return 0, errors.New("cbor: " + t.String() + " length " + strconv.FormatUint(val, 10) + " is too large, causing integer overflow")
return 0, 0, errors.New("cbor: " + t.String() + " length " + strconv.FormatUint(val, 10) + " is too large, causing integer overflow")
}
if len(data)-off < valInt { // valInt+off may overflow integer
return 0, io.ErrUnexpectedEOF
return 0, 0, io.ErrUnexpectedEOF
}
off += valInt
case cborTypeArray, cborTypeMap:
valInt := int(val)
if valInt < 0 {
// Detect integer overflow
return 0, errors.New("cbor: " + t.String() + " length " + strconv.FormatUint(val, 10) + " is too large, causing integer overflow")
return 0, 0, errors.New("cbor: " + t.String() + " length " + strconv.FormatUint(val, 10) + " is too large, causing integer overflow")
}
count := 1
if t == cborTypeMap {
count = 2
}
maxDepth := depth
for j := 0; j < count; j++ {
for i := 0; i < valInt; i++ {
if off, err = valid(data, off); err != nil {
return 0, err
var d int
if off, d, err = valid(data, off, depth+1); err != nil {
return 0, 0, err
}
if d > maxDepth {
maxDepth = d // Save max depth
}
}
}
depth = maxDepth
case cborTypeTag:
// Scan nested tag numbers to avoid recursion.
for true {
if len(data)-off < 1 { // Tag number must be followed by tag content.
return 0, io.ErrUnexpectedEOF
return 0, 0, io.ErrUnexpectedEOF
}
if cborType(data[off]&0xE0) != cborTypeTag {
break
}
if off, _, _, _, err = validHead(data, off); err != nil {
return 0, err
return 0, 0, err
}
depth++
}
// Check tag content.
if off, err = valid(data, off); err != nil {
return 0, err
if off, depth, err = valid(data, off, depth); err != nil {
return 0, 0, err
}
}
return off, nil
return off, depth, nil
}

func validIndefinite(data []byte, off int, t cborType) (_ int, err error) {
isString := (t == cborTypeByteString) || (t == cborTypeTextString)
func validIndefiniteString(data []byte, off int, t cborType, depth int) (int, int, error) {
var err error
for true {
if len(data)-off < 1 {
return 0, io.ErrUnexpectedEOF
return 0, 0, io.ErrUnexpectedEOF
}
if data[off] == 0xFF {
off++
break
}
if isString {
// Peek ahead to get next type and indefinite length status.
nextType := cborType(data[off] & 0xE0)
if t != nextType {
return 0, &SyntaxError{"cbor: wrong element type " + nextType.String() + " for indefinite-length " + t.String()}
}
if (data[off] & 0x1F) == 31 {
return 0, &SyntaxError{"cbor: indefinite-length " + t.String() + " chunk is not definite-length"}
}
// Peek ahead to get next type and indefinite length status.
nt := cborType(data[off] & 0xE0)
if t != nt {
return 0, 0, &SyntaxError{"cbor: wrong element type " + nt.String() + " for indefinite-length " + t.String()}
}
if off, err = valid(data, off); err != nil {
return 0, err
if (data[off] & 0x1F) == 31 {
return 0, 0, &SyntaxError{"cbor: indefinite-length " + t.String() + " chunk is not definite-length"}
}
if t == cborTypeMap {
if off, err = valid(data, off); err != nil {
return 0, err
}
if off, depth, err = valid(data, off, depth); err != nil {
return 0, 0, err
}
}
return off, nil
return off, depth, nil
}

func validIndefiniteArrOrMap(data []byte, off int, t cborType, depth int) (int, int, error) {
var err error
maxDepth := depth
i := 0
for ; true; i++ {
if len(data)-off < 1 {
return 0, 0, io.ErrUnexpectedEOF
}
if data[off] == 0xFF {
off++
break
}
var d int
if off, d, err = valid(data, off, depth+1); err != nil {
return 0, 0, err
}
if d > maxDepth {
maxDepth = d
}
}
if t == cborTypeMap && i%2 == 1 {
return 0, 0, &SyntaxError{"cbor: unexpected \"break\" code"}
}
return off, maxDepth, nil
}

func validHead(data []byte, off int) (_ int, t cborType, ai byte, val uint64, err error) {
Expand Down

0 comments on commit 3aa4328

Please sign in to comment.