Skip to content

Commit 2e90ced

Browse files
committed
Add Decoder.MaxTableNesting; limit table nesting to 128 by default
toml currently allows infinite table nesting; a document in the form "a.«10,000 more» = 1" takes ~2 seconds to parse and uses 1.8G of memory. I suppose we can may maybe improve on that a bit, but I haven't even looked. Just limit the table nesting to 128 by default, which should be more than enough for practically all real-world TOML documents.
1 parent 8685fba commit 2e90ced

4 files changed

Lines changed: 55 additions & 9 deletions

File tree

‎decode.go‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,14 +118,21 @@ const (
118118
// This decoder does not handle cyclic types. Decode will not terminate if a
119119
// cyclic type is passed.
120120
type Decoder struct {
121-
r io.Reader
121+
r io.Reader
122+
maxNest int
122123
}
123124

124125
// NewDecoder creates a new Decoder.
125126
func NewDecoder(r io.Reader) *Decoder {
126-
return &Decoder{r: r}
127+
return &Decoder{r: r, maxNest: 128}
127128
}
128129

130+
// MaxTableNesting sets the maximum table nesting to prevent using an
131+
// inordinate amount of system resources on deeply nested tables.
132+
//
133+
// The default is 128. Set to <=0 to disable the limit.
134+
func (dec *Decoder) MaxTableNesting(l int) { dec.maxNest = l }
135+
129136
var (
130137
unmarshalToml = reflect.TypeOf((*Unmarshaler)(nil)).Elem()
131138
unmarshalText = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem()
@@ -164,7 +171,7 @@ func (dec *Decoder) Decode(v any) (MetaData, error) {
164171
return MetaData{}, err
165172
}
166173

167-
p, err := parse(string(data))
174+
p, err := parse(string(data), dec.maxNest)
168175
if err != nil {
169176
return MetaData{}, err
170177
}

‎decode_test.go‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1194,3 +1194,32 @@ func TestDecodeCustomStructMarkedDecoded(t *testing.T) {
11941194
t.Errorf("\ncustom decode leaves undecoded fields: %v\n", meta.Undecoded())
11951195
}
11961196
}
1197+
1198+
func TestMaxTableNesting(t *testing.T) {
1199+
tests := []struct {
1200+
doc string
1201+
maxNest int
1202+
wantErr string
1203+
}{
1204+
{"\n\n" + strings.Repeat(".a", 128)[1:] + " = 1", -99, ""},
1205+
{"\n\n" + strings.Repeat(".a", 129)[1:] + " = 1", -99, "toml: line 3: too many nested tables: can have up to 128 nested tables"},
1206+
{"\n\n" + strings.Repeat(".a", 300)[1:] + " = 1", 0, ""},
1207+
{"\n\n" + strings.Repeat(".a", 5)[1:] + " = 1", 5, ""},
1208+
{"\n\n" + strings.Repeat(".a", 6)[1:] + " = 1", 5, "toml: line 3: too many nested tables: can have up to 5 nested tables"},
1209+
}
1210+
1211+
for _, tt := range tests {
1212+
t.Run("", func(t *testing.T) {
1213+
d := NewDecoder(strings.NewReader(tt.doc))
1214+
if tt.maxNest != -99 {
1215+
d.MaxTableNesting(tt.maxNest)
1216+
}
1217+
1218+
var m map[string]any
1219+
_, err := d.Decode(&m)
1220+
if !errorContains(err, tt.wantErr) {
1221+
t.Fatalf("wrong error\nhave: %q\nwant: %q", err, tt.wantErr)
1222+
}
1223+
})
1224+
}
1225+
}

‎error.go‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,7 @@ func (pe ParseError) Error() string {
9595
if pe.LastKey == "" {
9696
return fmt.Sprintf("toml: line %d: %s", pe.Position.Line, pe.Message)
9797
}
98-
return fmt.Sprintf("toml: line %d (last key %q): %s",
99-
pe.Position.Line, pe.LastKey, pe.Message)
98+
return fmt.Sprintf("toml: line %d (last key %q): %s", pe.Position.Line, pe.LastKey, pe.Message)
10099
}
101100

102101
// ErrorWithPosition returns the error with detailed location context.

‎parse.go‎

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ import (
1313

1414
type parser struct {
1515
lx *lexer
16+
maxNest int
1617
context Key // Full key for the current hash in scope.
1718
currentKey string // Base key name for everything except hashes.
1819
pos Position // Current position in the TOML file.
1920

20-
ordered []Key // List of keys in the order that they appear in the TOML data.
21-
21+
ordered []Key // List of keys in the order that they appear in the TOML data.
2222
keyInfo map[string]keyInfo // Map keyname → info about the TOML key.
2323
mapping map[string]any // Map keyname → key value.
2424
implicits map[string]struct{} // Record implicit keys (e.g. "key.group.names").
@@ -29,7 +29,7 @@ type keyInfo struct {
2929
tomlType tomlType
3030
}
3131

32-
func parse(data string) (p *parser, err error) {
32+
func parse(data string, maxNest int) (p *parser, err error) {
3333
defer func() {
3434
if r := recover(); r != nil {
3535
if pErr, ok := r.(ParseError); ok {
@@ -67,9 +67,10 @@ func parse(data string) (p *parser, err error) {
6767
}
6868

6969
p = &parser{
70+
lx: lex(data),
71+
maxNest: maxNest,
7072
keyInfo: make(map[string]keyInfo),
7173
mapping: make(map[string]any),
72-
lx: lex(data),
7374
ordered: make([]Key, 0),
7475
implicits: make(map[string]struct{}),
7576
}
@@ -542,6 +543,16 @@ func numPeriodsOK(s string) bool {
542543
// Establishing the context also makes sure that the key isn't a duplicate, and
543544
// will create implicit hashes automatically.
544545
func (p *parser) addContext(key Key, array bool) {
546+
if p.maxNest > 0 && len(key) >= p.maxNest {
547+
// Don't use panicf() helper because we don't want to set LastKey here –
548+
// it's going to be a very long string and not very useful.
549+
panic(ParseError{
550+
Message: fmt.Sprintf("too many nested tables: can have up to %d nested tables", p.maxNest),
551+
Position: p.pos.withCol(p.lx.input),
552+
Line: p.pos.Line,
553+
})
554+
}
555+
545556
/// Always start at the top level and drill down for our context.
546557
hashContext := p.mapping
547558
keyContext := make(Key, 0, len(key)-1)

0 commit comments

Comments
 (0)