diff --git a/json/codec.go b/json/codec.go index 77fe264..c6490bf 100644 --- a/json/codec.go +++ b/json/codec.go @@ -43,6 +43,7 @@ type encoder struct { type decoder struct { flags ParseFlags + depth int } type ( diff --git a/json/nesting_depth_test.go b/json/nesting_depth_test.go new file mode 100644 index 0000000..0495f15 --- /dev/null +++ b/json/nesting_depth_test.go @@ -0,0 +1,97 @@ +package json + +import ( + "bytes" + "strings" + "testing" +) + +// These tests cover the nesting-depth guard added to the decoder. Without it, +// deeply nested input recurses through parseValue/parseArray/parseObject until +// the goroutine stack is exhausted, which is a fatal (unrecoverable) crash in Go +// rather than a returned error. The standard library's encoding/json applies the +// same maxNestingDepth limit. + +func nestedArray(depth int) []byte { + b := make([]byte, 0, depth*2) + b = append(b, bytes.Repeat([]byte("["), depth)...) + b = append(b, bytes.Repeat([]byte("]"), depth)...) + return b +} + +func nestedObject(depth int) []byte { + // {"a":{"a":{...:null...}}} + var b bytes.Buffer + for i := 0; i < depth; i++ { + b.WriteString(`{"a":`) + } + b.WriteString("null") + for i := 0; i < depth; i++ { + b.WriteByte('}') + } + return b.Bytes() +} + +func TestUnmarshalRejectsExcessiveArrayNesting(t *testing.T) { + var v interface{} + err := Unmarshal(nestedArray(maxNestingDepth+1), &v) + if err == nil { + t.Fatal("expected error for input nested beyond maxNestingDepth, got nil") + } + if !strings.Contains(err.Error(), "depth") { + t.Fatalf("expected a max-depth error, got: %v", err) + } +} + +func TestUnmarshalRejectsExcessiveObjectNesting(t *testing.T) { + var v interface{} + err := Unmarshal(nestedObject(maxNestingDepth+1), &v) + if err == nil { + t.Fatal("expected error for object nested beyond maxNestingDepth, got nil") + } + if !strings.Contains(err.Error(), "depth") { + t.Fatalf("expected a max-depth error, got: %v", err) + } +} + +// Nesting that reaches a deeply nested value inside an otherwise-skipped struct +// field must also be bounded (the value is consumed via parseValue). +func TestUnmarshalRejectsExcessiveNestingInSkippedField(t *testing.T) { + payload := append([]byte(`{"unknown":`), nestedArray(maxNestingDepth+1)...) + payload = append(payload, '}') + var v struct { + Known string `json:"known"` + } + if err := Unmarshal(payload, &v); err == nil { + t.Fatal("expected error for deeply nested value in skipped field, got nil") + } +} + +func TestUnmarshalAcceptsNestingWithinLimit(t *testing.T) { + var v interface{} + if err := Unmarshal(nestedArray(maxNestingDepth-1), &v); err != nil { + t.Fatalf("input within the depth limit should decode, got: %v", err) + } +} + +// A wide but shallow document (depth 2) must not be affected by the depth guard: +// the limit counts nesting level, not the number of sibling elements. +func TestUnmarshalAllowsWideShallowArray(t *testing.T) { + var b bytes.Buffer + b.WriteByte('[') + for i := 0; i < 100000; i++ { + if i > 0 { + b.WriteByte(',') + } + b.WriteByte('0') + } + b.WriteByte(']') + + var v []int + if err := Unmarshal(b.Bytes(), &v); err != nil { + t.Fatalf("wide shallow array should decode, got: %v", err) + } + if len(v) != 100000 { + t.Fatalf("expected 100000 elements, got %d", len(v)) + } +} diff --git a/json/parse.go b/json/parse.go index d0ee221..c19a7f9 100644 --- a/json/parse.go +++ b/json/parse.go @@ -691,11 +691,18 @@ func (d decoder) parseArray(b []byte) ([]byte, []byte, Kind, error) { } } +const maxNestingDepth = 10000 + func (d decoder) parseValue(b []byte) ([]byte, []byte, Kind, error) { if len(b) == 0 { return nil, b, Undefined, syntaxError(b, "unexpected end of JSON input") } + d.depth++ + if d.depth > maxNestingDepth { + return nil, b, Undefined, syntaxError(b, "exceeded maximum nesting depth") + } + var v []byte var k Kind var err error