From b8490c0dbddc5d5dd61f72addb672ab1adbebce5 Mon Sep 17 00:00:00 2001 From: Farhan Farasdak Date: Sat, 27 Jun 2026 21:11:16 +0700 Subject: [PATCH 1/2] patch nesting depth issue --- json/codec.go | 4 ++ json/nesting_depth_test.go | 97 ++++++++++++++++++++++++++++++++++++++ json/parse.go | 12 +++++ 3 files changed, 113 insertions(+) create mode 100644 json/nesting_depth_test.go diff --git a/json/codec.go b/json/codec.go index 77fe264f..ecaeac13 100644 --- a/json/codec.go +++ b/json/codec.go @@ -43,6 +43,10 @@ type encoder struct { type decoder struct { flags ParseFlags + // depth tracks the current JSON nesting depth while decoding so that + // deeply nested input cannot exhaust the goroutine stack (see + // maxNestingDepth). Mirrors the protection in the standard library. + depth int } type ( diff --git a/json/nesting_depth_test.go b/json/nesting_depth_test.go new file mode 100644 index 00000000..0495f154 --- /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 d0ee2214..bdec3f04 100644 --- a/json/parse.go +++ b/json/parse.go @@ -691,11 +691,23 @@ func (d decoder) parseArray(b []byte) ([]byte, []byte, Kind, error) { } } +// maxNestingDepth bounds how deeply nested a JSON document may be before +// decoding fails. Without it, parseValue/parseArray/parseObject recurse without +// limit on attacker-controlled input and exhaust the goroutine stack, which is a +// fatal, unrecoverable crash in Go (an unauthenticated denial of service). The +// value matches the limit used by the standard library's encoding/json. +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 From 9ff4d76403cb3b88a4ad1bbb728c32426b956550 Mon Sep 17 00:00:00 2001 From: Farhan Farasdak Date: Sat, 27 Jun 2026 21:27:13 +0700 Subject: [PATCH 2/2] remove comment --- json/codec.go | 3 --- json/parse.go | 5 ----- 2 files changed, 8 deletions(-) diff --git a/json/codec.go b/json/codec.go index ecaeac13..c6490bfe 100644 --- a/json/codec.go +++ b/json/codec.go @@ -43,9 +43,6 @@ type encoder struct { type decoder struct { flags ParseFlags - // depth tracks the current JSON nesting depth while decoding so that - // deeply nested input cannot exhaust the goroutine stack (see - // maxNestingDepth). Mirrors the protection in the standard library. depth int } diff --git a/json/parse.go b/json/parse.go index bdec3f04..c19a7f9d 100644 --- a/json/parse.go +++ b/json/parse.go @@ -691,11 +691,6 @@ func (d decoder) parseArray(b []byte) ([]byte, []byte, Kind, error) { } } -// maxNestingDepth bounds how deeply nested a JSON document may be before -// decoding fails. Without it, parseValue/parseArray/parseObject recurse without -// limit on attacker-controlled input and exhaust the goroutine stack, which is a -// fatal, unrecoverable crash in Go (an unauthenticated denial of service). The -// value matches the limit used by the standard library's encoding/json. const maxNestingDepth = 10000 func (d decoder) parseValue(b []byte) ([]byte, []byte, Kind, error) {