go-sdk/internal/json/json_test.go
Yaroslav b55bcbd610
internal/json: json nesting depth check (#1200)
Added a lightweight JSON depth-checking pass before the actual parsing.
2026-08-27 10:47:08 +02:00

259 lines
5.9 KiB
Go

// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
// Use of this source code is governed by the license
// that can be found in the LICENSE file.
package json
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"strings"
"testing"
"time"
"github.com/google/go-cmp/cmp"
)
func TestUnmarshalCaseSensitivity(t *testing.T) {
type Nested struct {
Field string `json:"field"`
}
type Target struct {
Field string
TaggedField string `json:"custom_tag"`
Nested *Nested
}
tests := []struct {
name string
json string
want Target
}{
{
name: "exact match",
json: `{"Field": "value", "custom_tag": "tagged", "Nested": {"field": "nested"}}`,
want: Target{
Field: "value",
TaggedField: "tagged",
Nested: &Nested{
Field: "nested",
},
},
},
{
name: "case mismatch",
json: `{"field": "value", "Custom_tag": "tagged", "Nested": {"Field": "nested"}}`,
want: Target{
Field: "",
TaggedField: "",
Nested: &Nested{
Field: "",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var got Target
if err := Unmarshal([]byte(tt.json), &got); err != nil {
t.Fatalf("Unmarshal failed: %v", err)
}
if diff := cmp.Diff(tt.want, got); diff != "" {
t.Errorf("Unmarshal mismatch (-want +got):\n%s", diff)
}
})
}
}
func TestNewDecoderCaseSensitivity(t *testing.T) {
type Target struct {
Field string `json:"field"`
TaggedField string `json:"custom_tag"`
}
tests := []struct {
name string
json string
want Target
}{
{
name: "exact match",
json: `{"field": "value", "custom_tag": "tagged"}`,
want: Target{Field: "value", TaggedField: "tagged"},
},
{
name: "case mismatch",
json: `{"Field": "value", "Custom_tag": "tagged"}`,
want: Target{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var got Target
dec := NewDecoder(strings.NewReader(tt.json))
if err := dec.Decode(&got); err != nil {
t.Fatalf("Decode failed: %v", err)
}
if diff := cmp.Diff(tt.want, got); diff != "" {
t.Errorf("Decode mismatch (-want +got):\n%s", diff)
}
})
}
}
func TestUnmarshalNullCharacter(t *testing.T) {
type Target struct {
Field string `json:"field"`
}
tests := []struct {
name string
json string
want string
}{
{
name: "null char in middle",
json: `{"fi\u0000eld": "value"}`,
},
{
name: "null char at end",
json: `{"field\u0000": "value"}`,
},
{
name: "null char at start",
json: `{"\u0000field": "value"}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var got Target
if err := Unmarshal([]byte(tt.json), &got); err != nil {
t.Fatalf("Unmarshal failed: %v", err)
}
if got.Field != "" {
t.Errorf("Field was set: %s", tt.json)
}
})
}
}
func TestUnmarshalChecksNestingDepth(t *testing.T) {
createNesting := func(depth int) []byte {
return []byte(strings.Repeat("[", depth) + strings.Repeat("]", depth))
}
tests := []struct {
name string
data []byte
wantErr bool
}{
{"flat object", []byte(`{"a":1}`), false},
{"at limit", createNesting(defaultMaxDepth), false},
{"one over limit", createNesting(defaultMaxDepth + 1), true},
{"brackets in strings do not count", []byte(`{"a":"[[[[[[[[[["}`), false},
{"escaped quote in string", []byte(`{"a":"\"[[[[[["}`), false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var v any
err := Unmarshal(tt.data, &v)
gotDepthErr := errors.Is(err, errMaxDepthExceeded)
if gotDepthErr != tt.wantErr {
t.Fatalf("Unmarshal() error = %v (err=%v), want depth error = %v", gotDepthErr, err, tt.wantErr)
}
})
}
}
func TestUnmarshalMaxDepthExitsFast(t *testing.T) {
data := []byte(strings.Repeat("[", 500_000) + strings.Repeat("]", 500_000))
done := make(chan error, 1)
go func() {
var v any
done <- Unmarshal(data, &v)
}()
select {
case err := <-done:
if !errors.Is(err, errMaxDepthExceeded) {
t.Fatalf("Unmarshal error = %v, want %v", err, errMaxDepthExceeded)
}
case <-time.After(500 * time.Millisecond):
t.Fatal("Unmarshal did not reject deeply nested payload promptly")
}
}
func FuzzUnmarshalKeyComparison(f *testing.F) {
// Add seed corpus for expected valid key and the known problematic cases.
f.Add("field")
f.Add("FIELD")
f.Add("field\x00")
f.Fuzz(func(t *testing.T, key string) {
// Generate valid JSON with the fuzzed key.
// json.Encoder ensures the key is properly escaped (e.g. \x00 becomes \u0000).
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetEscapeHTML(false)
if err := enc.Encode(key); err != nil {
return
}
encodedKey := buf.Bytes()
jsonData := fmt.Sprintf(`{%s: "value"}`, encodedKey)
type Target struct {
Field string `json:"field"`
}
var got Target
if err := Unmarshal([]byte(jsonData), &got); err != nil {
return
}
if got.Field == "value" && key != "field" {
t.Errorf("Unmarshal matched key %q (JSON: %s) to field 'field'", key, encodedKey)
}
})
}
func FuzzUnmarshalFieldIsolation(f *testing.F) {
// Add seed corpus.
f.Add("other", "value")
f.Add("safe", "overwrite")
f.Add("safe\x00", "attack")
f.Fuzz(func(t *testing.T, key, val string) {
// Generate valid JSON with the fuzzed key and value.
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetEscapeHTML(false)
if err := enc.Encode(key); err != nil {
return
}
encodedKey := buf.Bytes()
buf.Reset()
if err := enc.Encode(val); err != nil {
return
}
encodedVal := buf.Bytes()
jsonData := fmt.Sprintf(`{"safe": "value", %s: %s}`, encodedKey, encodedVal)
type Target struct {
Safe string `json:"safe"`
}
var got Target
if err := Unmarshal([]byte(jsonData), &got); err != nil {
return
}
if got.Safe != "value" && key != "safe" {
t.Errorf("Field 'safe' improperly modified by key %q (JSON: %s). Got %q, want %q", key, encodedKey, got.Safe, "value")
}
})
}