VYPR
High severity7.5NVD Advisory· Published Jan 23, 2026· Updated Apr 9, 2026

CVE-2026-0994

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.

PackageAffected versionsPatched versions
protobufPyPI
>= 6.30.0rc1, < 6.33.56.33.5
protobufPyPI
< 5.29.65.29.6

Affected products

1

Patches

2
5ebddcb1bcbe

Fix 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)
    
d2b001626d13

Fix Any recursion depth bypass in Python json_format.ParseDict (#25239)

https://github.com/protocolbuffers/protobufAviral GargJan 29, 2026via ghsa
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

News mentions

0

No linked articles in our index yet.