From 38c2e552baa3eed0c50d8a1d878ffc1634b0768f Mon Sep 17 00:00:00 2001
From: aviralgarg05 <gargaviral99@gmail.com>
Date: Fri, 9 Jan 2026 20:59:10 +0530
Subject: [PATCH] Fix Any recursion depth bypass in Python
 json_format.ParseDict

This fixes a security vulnerability where nested google.protobuf.Any messages
could bypass the max_recursion_depth limit, potentially leading to denial of
service via stack overflow.

The root cause was that _ConvertAnyMessage() was calling itself recursively
via methodcaller() for nested well-known types, bypassing the recursion depth
tracking in ConvertMessage().

The fix routes well-known type parsing through ConvertMessage() to ensure
proper recursion depth accounting for all message types including nested Any.

Fixes #25070
---
 .../protobuf/internal/json_format_test.py     | 150 +++++++++++++++++-
 python/google/protobuf/json_format.py         |  12 +-
 2 files changed, 157 insertions(+), 5 deletions(-)

diff --git a/python/google/protobuf/internal/json_format_test.py b/python/google/protobuf/internal/json_format_test.py
index b4f516d83..74c45f217 100644
--- a/python/google/protobuf/internal/json_format_test.py
+++ b/python/google/protobuf/internal/json_format_test.py
@@ -1295,8 +1295,154 @@ class JsonFormatTest(JsonFormatBase):
         message,
         max_recursion_depth=3)
     # The following one can pass
-    json_format.Parse('{"payload": {}, "child": {"child":{}}}',
-                      message, max_recursion_depth=3)
+    json_format.Parse(
+        '{"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(
+        json.loads('{"old_value": 2}'),
+        json.loads(json_format.MessageToJson(message)),
+    )
+
+    new_message = more_messages_pb2.ConflictJsonName(new_value=2)
+    self.assertEqual(
+        json.loads('{"value": 2}'),
+        json.loads(json_format.MessageToJson(new_message)),
+    )
+
+  def testJsonNameConflictParse(self):
+    message = more_messages_pb2.ConflictJsonName()
+    json_format.Parse('{"value": 2}', message)
+    self.assertEqual(2, message.new_value)
+    self.assertEqual(0, message.value)
+
+  def testJsonNameConflictRoundTrip(self):
+    message = more_messages_pb2.ConflictJsonName(value=2)
+    parsed_message = more_messages_pb2.ConflictJsonName()
+    json_string = json_format.MessageToJson(message)
+    json_format.Parse(json_string, parsed_message)
+    self.assertEqual(message, parsed_message)
+
+    new_message = more_messages_pb2.ConflictJsonName(new_value=2)
+    new_parsed_message = more_messages_pb2.ConflictJsonName()
+    json_string = json_format.MessageToJson(new_message)
+    json_format.Parse(json_string, new_parsed_message)
+    self.assertEqual(new_message, new_parsed_message)
+
+  def testOtherParseErrors(self):
+    self.CheckError(
+        '9',
+        "Failed to parse JSON: TypeError: 'int' object is not iterable.",
+    )
+
+  def testManyRecursionsRaisesParseError(self):
+    num_recursions = 1050
+    text = ('{"a":' * num_recursions) + '""' + ('}' * num_recursions)
+    with self.assertRaises(json_format.ParseError):
+      json_format.Parse(text, json_format_proto3_pb2.TestMessage())
 
 if __name__ == '__main__':
   unittest.main()
diff --git a/python/google/protobuf/json_format.py b/python/google/protobuf/json_format.py
index a04e8aef1..2fe8da64c 100644
--- a/python/google/protobuf/json_format.py
+++ b/python/google/protobuf/json_format.py
@@ -496,6 +496,10 @@ class _Parser(object):
     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('Message too deep. Max recursion depth is {0}'.format(
@@ -669,9 +673,11 @@ class _Parser(object):
       self._ConvertWrapperMessage(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)
-- 
2.52.0

