File CVE-2026-0994.patch of Package protobuf.42448
From ec3bb5e719e87745f54e2728819a3206076cc365 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 | 101 ++++++++++++++++++
python/google/protobuf/json_format.py | 12 ++-
2 files changed, 110 insertions(+), 3 deletions(-)
diff --git a/python/google/protobuf/internal/json_format_test.py b/python/google/protobuf/internal/json_format_test.py
index 15b376c18..2994a08de 100644
--- a/python/google/protobuf/internal/json_format_test.py
+++ b/python/google/protobuf/internal/json_format_test.py
@@ -1276,6 +1276,107 @@ class JsonFormatTest(JsonFormatBase):
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(
diff --git a/python/google/protobuf/json_format.py b/python/google/protobuf/json_format.py
index 1b6ce9d03..569002b6a 100644
--- a/python/google/protobuf/json_format.py
+++ b/python/google/protobuf/json_format.py
@@ -471,6 +471,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(
@@ -644,9 +648,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