Skip to content

Commit 235abcd

Browse files
committed
fix python text_format unbounded recursion
1 parent b7ccafe commit 235abcd

2 files changed

Lines changed: 136 additions & 18 deletions

File tree

python/google/protobuf/internal/text_format_test.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -673,6 +673,76 @@ def testEscapedUtf8ASCIIRoundTrip(self, message_module):
673673
@parameterized.parameters(unittest_pb2, unittest_proto3_arena_pb2)
674674
class TextFormatParserTests(TextFormatBase):
675675

676+
def testParseRecursionDepthLimit(self, message_module):
677+
too_deep_text = textwrap.dedent("""\
678+
child {
679+
payload {}
680+
child {
681+
payload {}
682+
child {}
683+
}
684+
}
685+
""")
686+
self.assertRaisesRegex(
687+
text_format.ParseError,
688+
'Message too deep. Max recursion depth is 3',
689+
text_format.Parse,
690+
too_deep_text,
691+
message_module.NestedTestAllTypes(),
692+
max_recursion_depth=3,
693+
)
694+
shallow_text = textwrap.dedent("""\
695+
payload {}
696+
child {
697+
payload {}
698+
}
699+
""")
700+
text_format.Parse(
701+
shallow_text, message_module.NestedTestAllTypes(), max_recursion_depth=3
702+
)
703+
704+
def testParseAnyRecursionDepthLimit(self, message_module):
705+
del message_module
706+
message = any_pb2.Any()
707+
text = (
708+
'[type.googleapis.com/google.protobuf.Any] {\n'
709+
' [type.googleapis.com/google.protobuf.Any] {\n'
710+
' [type.googleapis.com/google.protobuf.Any] {}\n'
711+
' }\n'
712+
'}\n'
713+
)
714+
715+
with self.assertRaisesRegex(
716+
text_format.ParseError,
717+
'Message too deep. Max recursion depth is 2',
718+
):
719+
text_format.Parse(
720+
text,
721+
message,
722+
descriptor_pool=descriptor_pool.Default(),
723+
max_recursion_depth=2,
724+
)
725+
726+
text_format.Parse(
727+
text,
728+
any_pb2.Any(),
729+
descriptor_pool=descriptor_pool.Default(),
730+
max_recursion_depth=4,
731+
)
732+
733+
def testParseDefaultBehaviorRemainsUnbounded(self, message_module):
734+
del message_module
735+
message = descriptor_pb2.DescriptorProto()
736+
text = 'nested_type {' * 110 + '}' * 110
737+
738+
try:
739+
text_format.Parse(text, message)
740+
except Exception as exc: # noqa: BLE001
741+
self.fail(
742+
'expected default parsing to remain unbounded, '
743+
f'got {type(exc).__name__}: {exc}'
744+
)
745+
676746
def testParseAllFields(self, message_module):
677747
message = message_module.TestAllTypes()
678748
test_util.SetAllFields(message)

python/google/protobuf/text_format.py

Lines changed: 66 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -633,7 +633,8 @@ def Parse(text,
633633
allow_unknown_extension=False,
634634
allow_field_number=False,
635635
descriptor_pool=None,
636-
allow_unknown_field=False):
636+
allow_unknown_field=False,
637+
max_recursion_depth=None):
637638
"""Parses a text representation of a protocol message into a message.
638639
639640
NOTE: for historical reasons this function does not clear the input
@@ -671,6 +672,9 @@ def Parse(text,
671672
allow_unknown_field: if True, skip over unknown field and keep
672673
parsing. Avoid to use this option if possible. It may hide some
673674
errors (e.g. spelling error on field name)
675+
max_recursion_depth: Optional maximum recursion depth of a text proto
676+
message to be deserialized. Text proto messages over this depth will
677+
fail to parse. ``None`` keeps the historical unbounded behavior.
674678
675679
Returns:
676680
Message: The same message passed as argument.
@@ -683,15 +687,17 @@ def Parse(text,
683687
allow_unknown_extension,
684688
allow_field_number,
685689
descriptor_pool=descriptor_pool,
686-
allow_unknown_field=allow_unknown_field)
690+
allow_unknown_field=allow_unknown_field,
691+
max_recursion_depth=max_recursion_depth)
687692

688693

689694
def Merge(text,
690695
message,
691696
allow_unknown_extension=False,
692697
allow_field_number=False,
693698
descriptor_pool=None,
694-
allow_unknown_field=False):
699+
allow_unknown_field=False,
700+
max_recursion_depth=None):
695701
"""Parses a text representation of a protocol message into a message.
696702
697703
Like Parse(), but allows repeated values for a non-repeated field, and uses
@@ -708,6 +714,9 @@ def Merge(text,
708714
allow_unknown_field: if True, skip over unknown field and keep
709715
parsing. Avoid to use this option if possible. It may hide some
710716
errors (e.g. spelling error on field name)
717+
max_recursion_depth: Optional maximum recursion depth of a text proto
718+
message to be deserialized. Text proto messages over this depth will
719+
fail to parse. ``None`` keeps the historical unbounded behavior.
711720
712721
Returns:
713722
Message: The same message passed as argument.
@@ -721,15 +730,17 @@ def Merge(text,
721730
allow_unknown_extension,
722731
allow_field_number,
723732
descriptor_pool=descriptor_pool,
724-
allow_unknown_field=allow_unknown_field)
733+
allow_unknown_field=allow_unknown_field,
734+
max_recursion_depth=max_recursion_depth)
725735

726736

727737
def ParseLines(lines,
728738
message,
729739
allow_unknown_extension=False,
730740
allow_field_number=False,
731741
descriptor_pool=None,
732-
allow_unknown_field=False):
742+
allow_unknown_field=False,
743+
max_recursion_depth=None):
733744
"""Parses a text representation of a protocol message into a message.
734745
735746
See Parse() for caveats.
@@ -744,6 +755,9 @@ def ParseLines(lines,
744755
allow_unknown_field: if True, skip over unknown field and keep
745756
parsing. Avoid to use this option if possible. It may hide some
746757
errors (e.g. spelling error on field name)
758+
max_recursion_depth: Optional maximum recursion depth of a text proto
759+
message to be deserialized. Text proto messages over this depth will
760+
fail to parse. ``None`` keeps the historical unbounded behavior.
747761
748762
Returns:
749763
The same message passed as argument.
@@ -754,7 +768,8 @@ def ParseLines(lines,
754768
parser = _Parser(allow_unknown_extension,
755769
allow_field_number,
756770
descriptor_pool=descriptor_pool,
757-
allow_unknown_field=allow_unknown_field)
771+
allow_unknown_field=allow_unknown_field,
772+
max_recursion_depth=max_recursion_depth)
758773
return parser.ParseLines(lines, message)
759774

760775

@@ -763,7 +778,8 @@ def MergeLines(lines,
763778
allow_unknown_extension=False,
764779
allow_field_number=False,
765780
descriptor_pool=None,
766-
allow_unknown_field=False):
781+
allow_unknown_field=False,
782+
max_recursion_depth=None):
767783
"""Parses a text representation of a protocol message into a message.
768784
769785
See Merge() for more details.
@@ -778,6 +794,9 @@ def MergeLines(lines,
778794
allow_unknown_field: if True, skip over unknown field and keep
779795
parsing. Avoid to use this option if possible. It may hide some
780796
errors (e.g. spelling error on field name)
797+
max_recursion_depth: Optional maximum recursion depth of a text proto
798+
message to be deserialized. Text proto messages over this depth will
799+
fail to parse. ``None`` keeps the historical unbounded behavior.
781800
782801
Returns:
783802
The same message passed as argument.
@@ -788,7 +807,8 @@ def MergeLines(lines,
788807
parser = _Parser(allow_unknown_extension,
789808
allow_field_number,
790809
descriptor_pool=descriptor_pool,
791-
allow_unknown_field=allow_unknown_field)
810+
allow_unknown_field=allow_unknown_field,
811+
max_recursion_depth=max_recursion_depth)
792812
return parser.MergeLines(lines, message)
793813

794814

@@ -799,11 +819,14 @@ def __init__(self,
799819
allow_unknown_extension=False,
800820
allow_field_number=False,
801821
descriptor_pool=None,
802-
allow_unknown_field=False):
822+
allow_unknown_field=False,
823+
max_recursion_depth=None):
803824
self.allow_unknown_extension = allow_unknown_extension
804825
self.allow_field_number = allow_field_number
805826
self.descriptor_pool = descriptor_pool
806827
self.allow_unknown_field = allow_unknown_field
828+
self.max_recursion_depth = max_recursion_depth
829+
self.recursion_depth = 0
807830

808831
def ParseLines(self, lines, message):
809832
"""Parses a text representation of a protocol message into a message."""
@@ -837,8 +860,38 @@ def _ParseOrMerge(self, lines, message):
837860
raise ParseError from e
838861
if message:
839862
self.root_type = message.DESCRIPTOR.full_name
863+
self.recursion_depth += 1
864+
if (
865+
self.max_recursion_depth is not None
866+
and self.recursion_depth > self.max_recursion_depth
867+
):
868+
raise ParseError(
869+
'Message too deep. Max recursion depth is {0}'.format(
870+
self.max_recursion_depth
871+
)
872+
)
840873
while not tokenizer.AtEnd():
841874
self._MergeField(tokenizer, message)
875+
self.recursion_depth -= 1
876+
877+
def _MergeMessage(self, tokenizer, message, end_token):
878+
self.recursion_depth += 1
879+
if (
880+
self.max_recursion_depth is not None
881+
and self.recursion_depth > self.max_recursion_depth
882+
):
883+
raise ParseError(
884+
'Message too deep. Max recursion depth is {0}'.format(
885+
self.max_recursion_depth
886+
)
887+
)
888+
while not tokenizer.TryConsume(end_token):
889+
if tokenizer.AtEnd():
890+
raise tokenizer.ParseErrorPreviousToken(
891+
'Expected "%s".' % (end_token,)
892+
)
893+
self._MergeField(tokenizer, message)
894+
self.recursion_depth -= 1
842895

843896
def _MergeField(self, tokenizer, message):
844897
"""Merges a single protocol message field into a message.
@@ -873,11 +926,9 @@ def _MergeField(self, tokenizer, message):
873926
if expanded_any_sub_message is None:
874927
raise ParseError('Type %s not found in descriptor pool' %
875928
packed_type_name)
876-
while not tokenizer.TryConsume(expanded_any_end_token):
877-
if tokenizer.AtEnd():
878-
raise tokenizer.ParseErrorPreviousToken('Expected "%s".' %
879-
(expanded_any_end_token,))
880-
self._MergeField(tokenizer, expanded_any_sub_message)
929+
self._MergeMessage(
930+
tokenizer, expanded_any_sub_message, expanded_any_end_token
931+
)
881932
deterministic = False
882933

883934
message.Pack(
@@ -1095,10 +1146,7 @@ def _MergeMessageField(self, tokenizer, message, field):
10951146
sub_message = getattr(message, field.name)
10961147
sub_message.SetInParent()
10971148

1098-
while not tokenizer.TryConsume(end_token):
1099-
if tokenizer.AtEnd():
1100-
raise tokenizer.ParseErrorPreviousToken('Expected "%s".' % (end_token,))
1101-
self._MergeField(tokenizer, sub_message)
1149+
self._MergeMessage(tokenizer, sub_message, end_token)
11021150

11031151
if is_map_entry:
11041152
value_cpptype = field.message_type.fields_by_name['value'].cpp_type

0 commit comments

Comments
 (0)