Skip to content

Commit 071a36c

Browse files
authored
fix: bound array and inline table nesting depth to prevent stack-overflow DoS (#1092)
1 parent 57fec25 commit 071a36c

2 files changed

Lines changed: 79 additions & 4 deletions

File tree

unmarshaler_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2588,6 +2588,53 @@ func TestUnmarshalErrors(t *testing.T) {
25882588
assert.Equal(t, "toml: cannot decode TOML integer into struct field toml_test.mystruct.Bar of type string", err.Error())
25892589
}
25902590

2591+
// TestUnmarshalDeeplyNestedValues checks that deeply nested arrays and inline
2592+
// tables are rejected with an ordinary error through the stable API, rather
2593+
// than overflowing the goroutine stack (an unrecoverable fatal error that no
2594+
// recover() can intercept and that would terminate the whole process).
2595+
func TestUnmarshalDeeplyNestedValues(t *testing.T) {
2596+
// Far beyond the parser's nesting limit, so this must error, not crash.
2597+
const depth = 100000
2598+
2599+
examples := []struct {
2600+
desc string
2601+
input string
2602+
}{
2603+
{
2604+
desc: "arrays",
2605+
input: "a=" + strings.Repeat("[", depth) + strings.Repeat("]", depth),
2606+
},
2607+
{
2608+
desc: "inline tables",
2609+
input: "a=" + strings.Repeat("{k=", depth) + "1" + strings.Repeat("}", depth),
2610+
},
2611+
}
2612+
2613+
for _, e := range examples {
2614+
t.Run("Unmarshal/"+e.desc, func(t *testing.T) {
2615+
var v interface{}
2616+
err := toml.Unmarshal([]byte(e.input), &v)
2617+
assert.Error(t, err)
2618+
})
2619+
2620+
t.Run("Decode/"+e.desc, func(t *testing.T) {
2621+
var v interface{}
2622+
err := toml.NewDecoder(strings.NewReader(e.input)).Decode(&v)
2623+
assert.Error(t, err)
2624+
})
2625+
}
2626+
2627+
// A modestly nested document must still decode without error: the guard
2628+
// only rejects pathologically deep input.
2629+
t.Run("valid nesting still decodes", func(t *testing.T) {
2630+
const n = 100
2631+
var v interface{}
2632+
input := "a=" + strings.Repeat("[", n) + strings.Repeat("]", n)
2633+
err := toml.Unmarshal([]byte(input), &v)
2634+
assert.NoError(t, err)
2635+
})
2636+
}
2637+
25912638
func TestUnmarshalStringInvalidStructField(t *testing.T) {
25922639
type Server struct {
25932640
Path string

unstable/parser.go

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,21 @@ type Parser struct {
5555
left []byte
5656
nodes []Node
5757
err error
58+
59+
// nesting is the current depth of nested arrays and inline tables being
60+
// parsed. It guards parseVal/parseValArray/parseInlineTable against
61+
// unbounded mutual recursion, which would otherwise let a deeply nested
62+
// document overflow the goroutine stack (an unrecoverable fatal error).
63+
nesting int
5864
}
5965

66+
// maxValueNesting is the maximum depth of nested arrays and inline tables the
67+
// parser accepts. Beyond it, parsing fails with a ParserError instead of
68+
// risking a stack overflow. Real-world TOML documents nest only a handful of
69+
// levels deep, so this bound is generous; it matches the limit used by the
70+
// standard library's encoding/json.
71+
const maxValueNesting = 10000
72+
6073
// Data returns the slice provided to the last call to Reset.
6174
func (p *Parser) Data() []byte {
6275
return p.data
@@ -92,6 +105,7 @@ func (p *Parser) Reset(b []byte) {
92105
p.left = b
93106
p.nodes = p.nodes[:0]
94107
p.err = nil
108+
p.nesting = 0
95109
}
96110

97111
// Error returns any error that has occurred during parsing.
@@ -513,10 +527,24 @@ func (p *Parser) parseVal(b []byte) (int32, []byte, error) {
513527
return p.parseKeyword(b, "inf", Float)
514528
case c == 'n':
515529
return p.parseKeyword(b, "nan", Float)
516-
case c == '[':
517-
return p.parseValArray(b)
518-
case c == '{':
519-
return p.parseInlineTable(b)
530+
case c == '[' || c == '{':
531+
// Arrays and inline tables recurse back into parseVal for each of
532+
// their elements. Bound that recursion so a document with millions of
533+
// nested brackets or braces cannot overflow the goroutine stack.
534+
if p.nesting >= maxValueNesting {
535+
return 0, nil, NewParserError(b[:1], "arrays and inline tables are nested more than the maximum of %d levels deep", maxValueNesting)
536+
}
537+
p.nesting++
538+
var h int32
539+
var rest []byte
540+
var err error
541+
if c == '[' {
542+
h, rest, err = p.parseValArray(b)
543+
} else {
544+
h, rest, err = p.parseInlineTable(b)
545+
}
546+
p.nesting--
547+
return h, rest, err
520548
case c == '+' || c == '-':
521549
return p.parseIntOrFloat(b)
522550
case c >= '0' && c <= '9':

0 commit comments

Comments
 (0)