CVE-2026-0994
Description
A denial-of-service (DoS) vulnerability exists in google.protobuf.json_format.ParseDict() in Python, where the max_recursion_depth limit can be bypassed when parsing nested google.protobuf.Any messages.
Due to missing recursion depth accounting inside the internal Any-handling logic, an attacker can supply deeply nested Any structures that bypass the intended recursion limit, eventually exhausting Python’s recursion stack and causing a RecursionError.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
protobufPyPI | >= 6.30.0rc1, < 6.33.5 | 6.33.5 |
protobufPyPI | < 5.29.6 | 5.29.6 |
Affected products
1Patches
25ebddcb1bcbeFix Any recursion depth bypass in Python json_format.ParseDict (#25239) (#25587)
2 files changed · +110 −6
python/google/protobuf/internal/json_format_test.py+101 −0 modified@@ -1630,6 +1630,107 @@ def testNestedRecursiveLimit(self): '{"payload": {}, "child": {"child":{}}}', message, max_recursion_depth=3 ) + def testAnyRecursionDepthEnforcement(self): + """Test that nested Any messages respect max_recursion_depth limit.""" + # Test that deeply nested Any messages raise ParseError instead of + # bypassing the recursion limit. This prevents DoS via nested Any. + message = any_pb2.Any() + + # Create nested Any structure that should exceed depth limit + # With max_recursion_depth=5, we can nest 4 Any messages + # (depth 1 = outer Any, depth 2-4 = nested Anys, depth 5 = final value) + nested_any = { + '@type': 'type.googleapis.com/google.protobuf.Any', + 'value': { + '@type': 'type.googleapis.com/google.protobuf.Any', + 'value': { + '@type': 'type.googleapis.com/google.protobuf.Any', + 'value': { + '@type': 'type.googleapis.com/google.protobuf.Any', + 'value': { + '@type': 'type.googleapis.com/google.protobuf.Any', + 'value': {}, + }, + }, + }, + }, + } + + # Should raise ParseError due to exceeding max depth, not RecursionError + self.assertRaisesRegex( + json_format.ParseError, + 'Message too deep. Max recursion depth is 5', + json_format.ParseDict, + nested_any, + message, + max_recursion_depth=5, + ) + + # Verify that Any messages within the limit can be parsed successfully + # With max_recursion_depth=5, we can nest up to 4 Any messages + shallow_any = { + '@type': 'type.googleapis.com/google.protobuf.Any', + 'value': { + '@type': 'type.googleapis.com/google.protobuf.Any', + 'value': { + '@type': 'type.googleapis.com/google.protobuf.Any', + 'value': { + '@type': 'type.googleapis.com/google.protobuf.Any', + 'value': {}, + }, + }, + }, + } + json_format.ParseDict(shallow_any, message, max_recursion_depth=5) + + def testAnyRecursionDepthBoundary(self): + """Test recursion depth boundary behavior (exclusive upper limit).""" + message = any_pb2.Any() + + # Create nested Any at depth exactly 4 (should succeed with max_recursion_depth=5) + depth_4_any = { + '@type': 'type.googleapis.com/google.protobuf.Any', + 'value': { + '@type': 'type.googleapis.com/google.protobuf.Any', + 'value': { + '@type': 'type.googleapis.com/google.protobuf.Any', + 'value': { + '@type': 'type.googleapis.com/google.protobuf.Any', + 'value': {}, + }, + }, + }, + } + # This should succeed: depth 4 < max_recursion_depth 5 + json_format.ParseDict(depth_4_any, message, max_recursion_depth=5) + + # Create nested Any at depth exactly 5 (should fail with max_recursion_depth=5) + depth_5_any = { + '@type': 'type.googleapis.com/google.protobuf.Any', + 'value': { + '@type': 'type.googleapis.com/google.protobuf.Any', + 'value': { + '@type': 'type.googleapis.com/google.protobuf.Any', + 'value': { + '@type': 'type.googleapis.com/google.protobuf.Any', + 'value': { + '@type': 'type.googleapis.com/google.protobuf.Any', + 'value': {}, + }, + }, + }, + }, + } + # This should fail: depth 5 == max_recursion_depth 5 (exclusive limit) + self.assertRaisesRegex( + json_format.ParseError, + 'Message too deep. Max recursion depth is 5', + json_format.ParseDict, + depth_5_any, + message, + max_recursion_depth=5, + ) + def testJsonNameConflictSerilize(self): message = more_messages_pb2.ConflictJsonName(value=2) self.assertEqual(
python/google/protobuf/json_format.py+9 −6 modified@@ -521,6 +521,10 @@ def ConvertMessage(self, value, message, path): Raises: ParseError: In case of convert problems. """ + # Increment recursion depth at message entry. The max_recursion_depth limit + # is exclusive: a depth value equal to max_recursion_depth will trigger an + # error. For example, with max_recursion_depth=5, nesting up to depth 4 is + # allowed, but attempting depth 5 raises ParseError. self.recursion_depth += 1 if self.recursion_depth > self.max_recursion_depth: raise ParseError( @@ -725,12 +729,11 @@ def _ConvertAnyMessage(self, value, message, path): value['value'], sub_message, '{0}.value'.format(path) ) elif full_name in _WKTJSONMETHODS: - methodcaller( - _WKTJSONMETHODS[full_name][1], - value['value'], - sub_message, - '{0}.value'.format(path), - )(self) + # For well-known types (including nested Any), use ConvertMessage + # to ensure recursion depth is properly tracked + self.ConvertMessage( + value['value'], sub_message, '{0}.value'.format(path) + ) else: del value['@type'] self._ConvertFieldValuePair(value, sub_message, path)
d2b001626d13Fix Any recursion depth bypass in Python json_format.ParseDict (#25239)
2 files changed · +110 −6
python/google/protobuf/internal/json_format_test.py+101 −0 modified@@ -1760,6 +1760,107 @@ def testNestedRecursiveLimit(self): '{"payload": {}, "child": {"child":{}}}', message, max_recursion_depth=3 ) + def testAnyRecursionDepthEnforcement(self): + """Test that nested Any messages respect max_recursion_depth limit.""" + # Test that deeply nested Any messages raise ParseError instead of + # bypassing the recursion limit. This prevents DoS via nested Any. + message = any_pb2.Any() + + # Create nested Any structure that should exceed depth limit + # With max_recursion_depth=5, we can nest 4 Any messages + # (depth 1 = outer Any, depth 2-4 = nested Anys, depth 5 = final value) + nested_any = { + '@type': 'type.googleapis.com/google.protobuf.Any', + 'value': { + '@type': 'type.googleapis.com/google.protobuf.Any', + 'value': { + '@type': 'type.googleapis.com/google.protobuf.Any', + 'value': { + '@type': 'type.googleapis.com/google.protobuf.Any', + 'value': { + '@type': 'type.googleapis.com/google.protobuf.Any', + 'value': {}, + }, + }, + }, + }, + } + + # Should raise ParseError due to exceeding max depth, not RecursionError + self.assertRaisesRegex( + json_format.ParseError, + 'Message too deep. Max recursion depth is 5', + json_format.ParseDict, + nested_any, + message, + max_recursion_depth=5, + ) + + # Verify that Any messages within the limit can be parsed successfully + # With max_recursion_depth=5, we can nest up to 4 Any messages + shallow_any = { + '@type': 'type.googleapis.com/google.protobuf.Any', + 'value': { + '@type': 'type.googleapis.com/google.protobuf.Any', + 'value': { + '@type': 'type.googleapis.com/google.protobuf.Any', + 'value': { + '@type': 'type.googleapis.com/google.protobuf.Any', + 'value': {}, + }, + }, + }, + } + json_format.ParseDict(shallow_any, message, max_recursion_depth=5) + + def testAnyRecursionDepthBoundary(self): + """Test recursion depth boundary behavior (exclusive upper limit).""" + message = any_pb2.Any() + + # Create nested Any at depth exactly 4 (should succeed with max_recursion_depth=5) + depth_4_any = { + '@type': 'type.googleapis.com/google.protobuf.Any', + 'value': { + '@type': 'type.googleapis.com/google.protobuf.Any', + 'value': { + '@type': 'type.googleapis.com/google.protobuf.Any', + 'value': { + '@type': 'type.googleapis.com/google.protobuf.Any', + 'value': {}, + }, + }, + }, + } + # This should succeed: depth 4 < max_recursion_depth 5 + json_format.ParseDict(depth_4_any, message, max_recursion_depth=5) + + # Create nested Any at depth exactly 5 (should fail with max_recursion_depth=5) + depth_5_any = { + '@type': 'type.googleapis.com/google.protobuf.Any', + 'value': { + '@type': 'type.googleapis.com/google.protobuf.Any', + 'value': { + '@type': 'type.googleapis.com/google.protobuf.Any', + 'value': { + '@type': 'type.googleapis.com/google.protobuf.Any', + 'value': { + '@type': 'type.googleapis.com/google.protobuf.Any', + 'value': {}, + }, + }, + }, + }, + } + # This should fail: depth 5 == max_recursion_depth 5 (exclusive limit) + self.assertRaisesRegex( + json_format.ParseError, + 'Message too deep. Max recursion depth is 5', + json_format.ParseDict, + depth_5_any, + message, + max_recursion_depth=5, + ) + def testJsonNameConflictSerilize(self): message = more_messages_pb2.ConflictJsonName(value=2) self.assertEqual(
python/google/protobuf/json_format.py+9 −6 modified@@ -524,6 +524,10 @@ def ConvertMessage(self, value, message, path): Raises: ParseError: In case of convert problems. """ + # Increment recursion depth at message entry. The max_recursion_depth limit + # is exclusive: a depth value equal to max_recursion_depth will trigger an + # error. For example, with max_recursion_depth=5, nesting up to depth 4 is + # allowed, but attempting depth 5 raises ParseError. self.recursion_depth += 1 if self.recursion_depth > self.max_recursion_depth: raise ParseError( @@ -744,12 +748,11 @@ def _ConvertAnyMessage(self, value, message, path): value['value'], sub_message, '{0}.value'.format(path) ) elif full_name in _WKTJSONMETHODS: - methodcaller( - _WKTJSONMETHODS[full_name][1], - value['value'], - sub_message, - '{0}.value'.format(path), - )(self) + # For well-known types (including nested Any), use ConvertMessage + # to ensure recursion depth is properly tracked + self.ConvertMessage( + value['value'], sub_message, '{0}.value'.format(path) + ) else: del value['@type'] try:
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/protocolbuffers/protobuf/pull/25239nvdPatchVendor AdvisoryWEB
- github.com/advisories/GHSA-7gcm-g887-7qv7ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-0994ghsaADVISORY
- github.com/protocolbuffers/protobuf/commit/5ebddcb1bcbe51d1fe323baa145e85f4f23128cfghsaWEB
- github.com/protocolbuffers/protobuf/commit/d2b001626d137c62dfee6c88c87324102531868bghsaWEB
- github.com/protocolbuffers/protobuf/issues/25070ghsaWEB
News mentions
0No linked articles in our index yet.