Fix CVE-2026-0994: nested Any messages bypassing recursion depth limits

Add max_recursion_depth parameter to json_format Parse/ParseDict functions
to prevent denial-of-service attacks via deeply nested protobuf messages.
The fix also changes _ConvertAnyMessage to use ConvertMessage directly,
ensuring recursion depth is properly tracked for nested Any messages.

Upstream PR: https://github.com/protocolbuffers/protobuf/pull/25239

New patches:
- protobuf-3.19-CVE-2026-0994-nested-any-recursion.patch
- protobuf-3.19-CVE-2026-0994-test.patch

Generated with [Claude Code](https://claude.ai/code)

Resolves: RHEL-144062

Signed-off-by: Adrian Reber <areber@redhat.com>
This commit is contained in:
Adrian Reber 2026-01-26 17:21:57 +00:00
parent fee26b568e
commit ea5b2395b2
3 changed files with 168 additions and 1 deletions

View File

@ -0,0 +1,125 @@
From: Fedora Package Maintainer
Subject: Fix nested Any messages bypassing recursion depth limits
Backport fix for CVE-2026-0994: nested google.protobuf.Any messages could
bypass recursion depth limits, potentially causing denial-of-service attacks.
This patch adds recursion depth tracking to the JSON parser and fixes the
_ConvertAnyMessage function to use ConvertMessage directly to ensure
recursion depth is properly tracked for all message types.
Upstream PR: https://github.com/protocolbuffers/protobuf/pull/25239
--- a/python/google/protobuf/json_format.py 2026-01-26 16:35:51.402115353 +0000
+++ b/python/google/protobuf/json_format.py 2026-01-26 16:41:58.361000000 +0000
@@ -400,7 +400,11 @@
return message_class()
-def Parse(text, message, ignore_unknown_fields=False, descriptor_pool=None):
+def Parse(text,
+ message,
+ ignore_unknown_fields=False,
+ descriptor_pool=None,
+ max_recursion_depth=100):
"""Parses a JSON representation of a protocol message into a message.
Args:
@@ -409,6 +413,9 @@
ignore_unknown_fields: If True, do not raise errors for unknown fields.
descriptor_pool: A Descriptor Pool for resolving types. If None use the
default.
+ max_recursion_depth: max recursion depth of JSON message to be
+ deserialized. JSON messages over this depth will fail to be
+ deserialized. Default value is 100.
Returns:
The same message passed as argument.
@@ -422,13 +429,15 @@
js = json.loads(text, object_pairs_hook=_DuplicateChecker)
except ValueError as e:
raise ParseError('Failed to load JSON: {0}.'.format(str(e)))
- return ParseDict(js, message, ignore_unknown_fields, descriptor_pool)
+ return ParseDict(js, message, ignore_unknown_fields, descriptor_pool,
+ max_recursion_depth)
def ParseDict(js_dict,
message,
ignore_unknown_fields=False,
- descriptor_pool=None):
+ descriptor_pool=None,
+ max_recursion_depth=100):
"""Parses a JSON dictionary representation into a message.
Args:
@@ -437,11 +446,14 @@
ignore_unknown_fields: If True, do not raise errors for unknown fields.
descriptor_pool: A Descriptor Pool for resolving types. If None use the
default.
+ max_recursion_depth: max recursion depth of JSON message to be
+ deserialized. JSON messages over this depth will fail to be
+ deserialized. Default value is 100.
Returns:
The same message passed as argument.
"""
- parser = _Parser(ignore_unknown_fields, descriptor_pool)
+ parser = _Parser(ignore_unknown_fields, descriptor_pool, max_recursion_depth)
parser.ConvertMessage(js_dict, message)
return message
@@ -452,9 +464,11 @@
class _Parser(object):
"""JSON format parser for protocol message."""
- def __init__(self, ignore_unknown_fields, descriptor_pool):
+ def __init__(self, ignore_unknown_fields, descriptor_pool, max_recursion_depth):
self.ignore_unknown_fields = ignore_unknown_fields
self.descriptor_pool = descriptor_pool
+ self.max_recursion_depth = max_recursion_depth
+ self.recursion_depth = 0
def ConvertMessage(self, value, message):
"""Convert a JSON object into a message.
@@ -466,14 +480,21 @@
Raises:
ParseError: In case of convert problems.
"""
- message_descriptor = message.DESCRIPTOR
- full_name = message_descriptor.full_name
- if _IsWrapperMessage(message_descriptor):
- self._ConvertWrapperMessage(value, message)
- elif full_name in _WKTJSONMETHODS:
- methodcaller(_WKTJSONMETHODS[full_name][1], value, message)(self)
- else:
- self._ConvertFieldValuePair(value, message)
+ self.recursion_depth += 1
+ if self.recursion_depth > self.max_recursion_depth:
+ raise ParseError('Message too deep. Max recursion depth is {0}'.format(
+ self.max_recursion_depth))
+ try:
+ message_descriptor = message.DESCRIPTOR
+ full_name = message_descriptor.full_name
+ if _IsWrapperMessage(message_descriptor):
+ self._ConvertWrapperMessage(value, message)
+ elif full_name in _WKTJSONMETHODS:
+ methodcaller(_WKTJSONMETHODS[full_name][1], value, message)(self)
+ else:
+ self._ConvertFieldValuePair(value, message)
+ finally:
+ self.recursion_depth -= 1
def _ConvertFieldValuePair(self, js, message):
"""Convert field value pairs into regular message.
@@ -608,8 +629,8 @@
if _IsWrapperMessage(message_descriptor):
self._ConvertWrapperMessage(value['value'], sub_message)
elif full_name in _WKTJSONMETHODS:
- methodcaller(
- _WKTJSONMETHODS[full_name][1], value['value'], sub_message)(self)
+ # Use ConvertMessage to ensure recursion depth is properly tracked
+ self.ConvertMessage(value['value'], sub_message)
else:
del value['@type']
self._ConvertFieldValuePair(value, sub_message)

View File

@ -0,0 +1,32 @@
From: Fedora Package Maintainer
Subject: Add test for CVE-2026-0994 recursion depth limits
Add test for the max_recursion_depth parameter to verify that deeply
nested messages properly trigger ParseError when exceeding the limit.
Upstream PR: https://github.com/protocolbuffers/protobuf/pull/25239
--- a/python/google/protobuf/internal/json_format_test.py 2026-01-26 16:51:53.848408566 +0000
+++ b/python/google/protobuf/internal/json_format_test.py 2026-01-26 16:52:13.501000000 +0000
@@ -1271,6 +1271,21 @@
'uint32Value': 4, 'stringValue': 'bla'},
indent=2, sort_keys=True))
+ def testNestedRecursiveLimit(self):
+ message = unittest_pb2.NestedTestAllTypes()
+ self.assertRaisesRegex(
+ json_format.ParseError,
+ 'Message too deep. Max recursion depth is 3',
+ json_format.Parse,
+ '{"child": {"child": {"child" : {}}}}',
+ message,
+ max_recursion_depth=3,
+ )
+ # The following one can pass
+ json_format.Parse(
+ '{"payload": {}, "child": {"child":{}}}', message, max_recursion_depth=3
+ )
+
if __name__ == '__main__':
unittest.main()

View File

@ -21,7 +21,7 @@ Name: protobuf
# “patch” updates of protobuf.
Version: 3.19.6
%global so_version 30
Release: 14%{?dist}
Release: 15%{?dist}
# The entire source is BSD-3-Clause, except the following files, which belong
# to the build system; are unpackaged maintainer utility scripts; or are used
@ -85,6 +85,11 @@ Patch3: protobuf-3.19.4-jre17-add-opens.patch
#
# The PyFrameObject structure members have been removed from the public C API.
Patch4: protobuf-3.19.4-python3.11.patch
# https://github.com/protocolbuffers/protobuf/pull/25239
# Fix nested Any messages bypassing recursion depth limits (CVE-2026-0994)
Patch5: protobuf-3.19-CVE-2026-0994-nested-any-recursion.patch
# Test for CVE-2026-0994 fix
Patch6: protobuf-3.19-CVE-2026-0994-test.patch
# A bundled copy of jsoncpp is included in the conformance tests, but the
# result is not packaged, so we do not treat it as a formal bundled
@ -290,6 +295,8 @@ descriptions in the Emacs editor.
%patch 2 -p0
%patch 3 -p1 -b .jre17
%patch 4 -p1 -b .python311
%patch 5 -p1 -b .CVE-2026-0994
%patch 6 -p1 -b .CVE-2026-0994-test
# Copy in the needed gtest/gmock implementations.
%setup -q -T -D -b 3 -n protobuf-%{version}%{?rcver}
@ -481,6 +488,9 @@ install -p -m 0644 %{SOURCE2} %{buildroot}%{_emacs_sitestartdir}
%changelog
* Mon Jan 26 2026 Adrian Reber <areber@redhat.com> - 3.19.6-15
- Fix CVE-2026-0994: nested Any messages bypassing recursion depth limits
* Wed Nov 12 2025 Adrian Reber <areber@redhat.com> - 3.19.6-14
- Disable tests during build that are flaky