internal/json: json nesting depth check (#1200)

Added a lightweight JSON depth-checking pass before the actual parsing.
This commit is contained in:
Yaroslav 2026-08-27 10:47:08 +02:00 • committed by GitHub
commit b55bcbd610
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -8,11 +8,21 @@ package json
import (
"bytes"
"fmt"
"io"
"github.com/segmentio/encoding/json"
)
// defaultMaxDepth is the maximum JSON nesting depth accepted by [Unmarshal].
// Realistic MCP payloads nest only a handful of levels, so 1000 is far more
// than any legitimate message requires while keeping worst-case parse cost
// bounded.
const defaultMaxDepth = 1000
// errMaxDepthExceeded is returned by [Unmarshal] when the input nests deeper than [defaultMaxDepth].
var errMaxDepthExceeded = fmt.Errorf("json: exceeded maximum nesting depth of %d", defaultMaxDepth)
type Decoder struct {
dec *json.Decoder
}
@ -28,5 +38,47 @@ func (d *Decoder) Decode(v any) error {
}
func Unmarshal(data []byte, v any) error {
if err := checkMaxDepth(data, defaultMaxDepth); err != nil {
return err
}
return NewDecoder(bytes.NewReader(data)).Decode(v)
}
// checkMaxDepth scans data once and reports [errMaxDepthExceeded] if the
// nesting of JSON objects and arrays exceeds maxDepth. It is a lightweight pass which
// tracks '{' and '[' against '}' and ']' while skipping over the contents of strings.
func checkMaxDepth(data []byte, maxDepth int) error {
var (
depth int
inString bool
escaped bool
)
for _, b := range data {
if inString {
switch {
case escaped:
escaped = false
case b == '\\':
escaped = true
case b == '"':
inString = false
}
continue
}
switch b {
case '"':
inString = true
case '{', '[':
depth++
if depth > maxDepth {
return errMaxDepthExceeded
}
case '}', ']':
if depth > 0 {
depth--
}
}
}
return nil
}