Skip to content

Commit bfcd647

Browse files
jhumpgopherbot
authored andcommitted
protojson: configurable recursion limit when unmarshalling
Fixes golang/protobuf#1583 and golang/protobuf#1584 Limits the level of recursion when parsing JSON to avoid fatal stack overflow errors if input uses pathologically deep nesting. This is already a feature of the binary format, and this adds that feature to the JSON format. This also re-implements how JSON values are discarded to be more efficient (and not use recursion). Change-Id: I4026b739abe0335387209a43645f65e4b6e43409 Reviewed-on: https://go-review.googlesource.com/c/protobuf/+/552255 LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: David Chase <[email protected]> Auto-Submit: Lasse Folger <[email protected]> Reviewed-by: Lasse Folger <[email protected]>
1 parent 24fba63 commit bfcd647

File tree

3 files changed

+109
-39
lines changed

3 files changed

+109
-39
lines changed

encoding/protojson/decode.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"strconv"
1212
"strings"
1313

14+
"google.golang.org/protobuf/encoding/protowire"
1415
"google.golang.org/protobuf/internal/encoding/json"
1516
"google.golang.org/protobuf/internal/encoding/messageset"
1617
"google.golang.org/protobuf/internal/errors"
@@ -47,6 +48,10 @@ type UnmarshalOptions struct {
4748
protoregistry.MessageTypeResolver
4849
protoregistry.ExtensionTypeResolver
4950
}
51+
52+
// RecursionLimit limits how deeply messages may be nested.
53+
// If zero, a default limit is applied.
54+
RecursionLimit int
5055
}
5156

5257
// Unmarshal reads the given []byte and populates the given [proto.Message]
@@ -67,6 +72,9 @@ func (o UnmarshalOptions) unmarshal(b []byte, m proto.Message) error {
6772
if o.Resolver == nil {
6873
o.Resolver = protoregistry.GlobalTypes
6974
}
75+
if o.RecursionLimit == 0 {
76+
o.RecursionLimit = protowire.DefaultRecursionLimit
77+
}
7078

7179
dec := decoder{json.NewDecoder(b), o}
7280
if err := dec.unmarshalMessage(m.ProtoReflect(), false); err != nil {
@@ -114,6 +122,10 @@ func (d decoder) syntaxError(pos int, f string, x ...interface{}) error {
114122

115123
// unmarshalMessage unmarshals a message into the given protoreflect.Message.
116124
func (d decoder) unmarshalMessage(m protoreflect.Message, skipTypeURL bool) error {
125+
d.opts.RecursionLimit--
126+
if d.opts.RecursionLimit < 0 {
127+
return errors.New("exceeded max recursion depth")
128+
}
117129
if unmarshal := wellKnownTypeUnmarshaler(m.Descriptor().FullName()); unmarshal != nil {
118130
return unmarshal(d, m)
119131
}

encoding/protojson/decode_test.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2489,6 +2489,87 @@ func TestUnmarshal(t *testing.T) {
24892489
inputText: `{"weak_message1":{"a":1}, "weak_message2":{"a":1}}`,
24902490
wantErr: `unknown field "weak_message2"`, // weak_message2 is unknown since the package containing it is not imported
24912491
skip: !flags.ProtoLegacy,
2492+
}, {
2493+
desc: "just at recursion limit: nested messages",
2494+
inputMessage: &testpb.TestAllTypes{},
2495+
inputText: `{"optionalNestedMessage":{"corecursive":{"optionalNestedMessage":{"corecursive":{}}}}}`,
2496+
umo: protojson.UnmarshalOptions{RecursionLimit: 5},
2497+
}, {
2498+
desc: "exceed recursion limit: nested messages",
2499+
inputMessage: &testpb.TestAllTypes{},
2500+
inputText: `{"optionalNestedMessage":{"corecursive":{"optionalNestedMessage":{"corecursive":{"optionalNestedMessage":{}}}}}}`,
2501+
umo: protojson.UnmarshalOptions{RecursionLimit: 5},
2502+
wantErr: "exceeded max recursion depth",
2503+
}, {
2504+
2505+
desc: "just at recursion limit: maps",
2506+
inputMessage: &testpb.TestAllTypes{},
2507+
inputText: `{"mapStringNestedMessage":{"key1":{"corecursive":{"mapStringNestedMessage":{}}}}}`,
2508+
umo: protojson.UnmarshalOptions{RecursionLimit: 3},
2509+
}, {
2510+
desc: "exceed recursion limit: maps",
2511+
inputMessage: &testpb.TestAllTypes{},
2512+
inputText: `{"mapStringNestedMessage":{"key1":{"corecursive":{"mapStringNestedMessage":{}}}}}`,
2513+
umo: protojson.UnmarshalOptions{RecursionLimit: 2},
2514+
wantErr: "exceeded max recursion depth",
2515+
}, {
2516+
desc: "just at recursion limit: arrays",
2517+
inputMessage: &testpb.TestAllTypes{},
2518+
inputText: `{"repeatedNestedMessage":[{"corecursive":{"repeatedInt32":[1,2,3]}}]}`,
2519+
umo: protojson.UnmarshalOptions{RecursionLimit: 3},
2520+
}, {
2521+
desc: "exceed recursion limit: arrays",
2522+
inputMessage: &testpb.TestAllTypes{},
2523+
inputText: `{"repeatedNestedMessage":[{"corecursive":{"repeatedNestedMessage":[{}]}}]}`,
2524+
umo: protojson.UnmarshalOptions{RecursionLimit: 3},
2525+
wantErr: "exceeded max recursion depth",
2526+
}, {
2527+
desc: "just at recursion limit: value",
2528+
inputMessage: &structpb.Value{},
2529+
inputText: `{"a":{"b":{"c":{"d":{}}}}}`,
2530+
umo: protojson.UnmarshalOptions{RecursionLimit: 5},
2531+
}, {
2532+
desc: "exceed recursion limit: value",
2533+
inputMessage: &structpb.Value{},
2534+
inputText: `{"a":{"b":{"c":{"d":{"e":[]}}}}}`,
2535+
umo: protojson.UnmarshalOptions{RecursionLimit: 5},
2536+
wantErr: "exceeded max recursion depth",
2537+
}, {
2538+
desc: "just at recursion limit: list value",
2539+
inputMessage: &structpb.ListValue{},
2540+
inputText: `[[[[[1, 2, 3, 4]]]]]`,
2541+
// Note: the JSON appears to have recursion of only 5. But it's actually 6 because the
2542+
// first leaf value (1) is actually a message (google.protobuf.Value), even though the
2543+
// JSON doesn't use an open brace.
2544+
umo: protojson.UnmarshalOptions{RecursionLimit: 6},
2545+
}, {
2546+
desc: "exceed recursion limit: list value",
2547+
inputMessage: &structpb.ListValue{},
2548+
inputText: `[[[[[1, 2, 3, 4, ["a", "b"]]]]]]`,
2549+
umo: protojson.UnmarshalOptions{RecursionLimit: 6},
2550+
wantErr: "exceeded max recursion depth",
2551+
}, {
2552+
desc: "just at recursion limit: struct value",
2553+
inputMessage: &structpb.Struct{},
2554+
inputText: `{"a":{"b":{"c":{"d":{}}}}}`,
2555+
umo: protojson.UnmarshalOptions{RecursionLimit: 5},
2556+
}, {
2557+
desc: "exceed recursion limit: struct value",
2558+
inputMessage: &structpb.Struct{},
2559+
inputText: `{"a":{"b":{"c":{"d":{"e":{}]}}}}}`,
2560+
umo: protojson.UnmarshalOptions{RecursionLimit: 5},
2561+
wantErr: "exceeded max recursion depth",
2562+
}, {
2563+
desc: "just at recursion limit: skip unknown",
2564+
inputMessage: &testpb.TestAllTypes{},
2565+
inputText: `{"foo":{"bar":[{"baz":{}}]}}`,
2566+
umo: protojson.UnmarshalOptions{RecursionLimit: 5, DiscardUnknown: true},
2567+
}, {
2568+
desc: "exceed recursion limit: skip unknown",
2569+
inputMessage: &testpb.TestAllTypes{},
2570+
inputText: `{"foo":{"bar":[{"baz":[{}]]}}`,
2571+
umo: protojson.UnmarshalOptions{RecursionLimit: 5, DiscardUnknown: true},
2572+
wantErr: "exceeded max recursion depth",
24922573
}}
24932574

24942575
for _, tt := range tests {

encoding/protojson/well_known_types.go

Lines changed: 16 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ func (d decoder) unmarshalAny(m protoreflect.Message) error {
176176
// Use another decoder to parse the unread bytes for @type field. This
177177
// avoids advancing a read from current decoder because the current JSON
178178
// object may contain the fields of the embedded type.
179-
dec := decoder{d.Clone(), UnmarshalOptions{}}
179+
dec := decoder{d.Clone(), UnmarshalOptions{RecursionLimit: d.opts.RecursionLimit}}
180180
tok, err := findTypeURL(dec)
181181
switch err {
182182
case errEmptyObject:
@@ -308,48 +308,25 @@ Loop:
308308
// array) in order to advance the read to the next JSON value. It relies on
309309
// the decoder returning an error if the types are not in valid sequence.
310310
func (d decoder) skipJSONValue() error {
311-
tok, err := d.Read()
312-
if err != nil {
313-
return err
314-
}
315-
// Only need to continue reading for objects and arrays.
316-
switch tok.Kind() {
317-
case json.ObjectOpen:
318-
for {
319-
tok, err := d.Read()
320-
if err != nil {
321-
return err
322-
}
323-
switch tok.Kind() {
324-
case json.ObjectClose:
325-
return nil
326-
case json.Name:
327-
// Skip object field value.
328-
if err := d.skipJSONValue(); err != nil {
329-
return err
330-
}
331-
}
311+
var open int
312+
for {
313+
tok, err := d.Read()
314+
if err != nil {
315+
return err
332316
}
333-
334-
case json.ArrayOpen:
335-
for {
336-
tok, err := d.Peek()
337-
if err != nil {
338-
return err
339-
}
340-
switch tok.Kind() {
341-
case json.ArrayClose:
342-
d.Read()
343-
return nil
344-
default:
345-
// Skip array item.
346-
if err := d.skipJSONValue(); err != nil {
347-
return err
348-
}
317+
switch tok.Kind() {
318+
case json.ObjectClose, json.ArrayClose:
319+
open--
320+
case json.ObjectOpen, json.ArrayOpen:
321+
open++
322+
if open > d.opts.RecursionLimit {
323+
return errors.New("exceeded max recursion depth")
349324
}
350325
}
326+
if open == 0 {
327+
return nil
328+
}
351329
}
352-
return nil
353330
}
354331

355332
// unmarshalAnyValue unmarshals the given custom-type message from the JSON

0 commit comments

Comments
 (0)