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.14-CVE-2026-0994-nested-any-recursion.patch - protobuf-3.14-CVE-2026-0994-test.patch Generated with [Claude Code](https://claude.ai/code) Resolves: RHEL-144068 Signed-off-by: Adrian Reber <areber@redhat.com>
This commit is contained in:
parent
15875dbe64
commit
9dfa2f5140
125
protobuf-3.14-CVE-2026-0994-nested-any-recursion.patch
Normal file
125
protobuf-3.14-CVE-2026-0994-nested-any-recursion.patch
Normal 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 17:47:01.782591911 +0000
|
||||
+++ b/python/google/protobuf/json_format.py 2026-01-26 17:58:45.049000000 +0000
|
||||
@@ -410,7 +410,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:
|
||||
@@ -419,6 +423,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.
|
||||
@@ -431,13 +438,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:
|
||||
@@ -446,11 +455,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
|
||||
|
||||
@@ -461,9 +473,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.
|
||||
@@ -475,14 +489,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.
|
||||
@@ -617,8 +638,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)
|
||||
32
protobuf-3.14-CVE-2026-0994-test.patch
Normal file
32
protobuf-3.14-CVE-2026-0994-test.patch
Normal 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 18:04:19.788000000 +0000
|
||||
+++ b/python/google/protobuf/internal/json_format_test.py 2026-01-26 18:04:40.403000000 +0000
|
||||
@@ -1258,6 +1258,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()
|
||||
@ -12,7 +12,7 @@
|
||||
Summary: Protocol Buffers - Google's data interchange format
|
||||
Name: protobuf
|
||||
Version: 3.14.0
|
||||
Release: 16%{?dist}
|
||||
Release: 17%{?dist}
|
||||
License: BSD
|
||||
URL: https://github.com/protocolbuffers/protobuf
|
||||
Source: https://github.com/protocolbuffers/protobuf/archive/v%{version}%{?rcver}/%{name}-%{version}%{?rcver}-all.tar.gz
|
||||
@ -33,6 +33,11 @@ Patch2: CVE-2021-22570.patch
|
||||
# https://issues.redhat.com/browse/RHEL-40872
|
||||
# Based on https://github.com/protocolbuffers/protobuf/pull/10542.patch
|
||||
Patch3: CVE-2022-1941.patch
|
||||
# Fix for CVE-2026-0994: nested Any messages bypassing recursion depth limits
|
||||
# https://github.com/protocolbuffers/protobuf/pull/25239
|
||||
Patch4: protobuf-3.14-CVE-2026-0994-nested-any-recursion.patch
|
||||
# Test for CVE-2026-0994 fix
|
||||
Patch5: protobuf-3.14-CVE-2026-0994-test.patch
|
||||
|
||||
BuildRequires: make
|
||||
BuildRequires: autoconf
|
||||
@ -223,6 +228,8 @@ descriptions in the Emacs editor.
|
||||
%patch -P 1 -p1
|
||||
%patch -P 2 -p1
|
||||
%patch -P 3 -p1
|
||||
%patch -P 4 -p1 -b .CVE-2026-0994
|
||||
%patch -P 5 -p1 -b .CVE-2026-0994-test
|
||||
mv googletest-5ec7f0c4a113e2f18ac2c6cc7df51ad6afc24081/* third_party/googletest/
|
||||
find -name \*.cc -o -name \*.h | xargs chmod -x
|
||||
chmod 644 examples/*
|
||||
@ -408,6 +415,9 @@ install -p -m 0644 %{SOURCE2} %{buildroot}%{_emacs_sitestartdir}
|
||||
|
||||
|
||||
%changelog
|
||||
* Mon Jan 26 2026 Adrian Reber <areber@redhat.com> - 3.14.0-17
|
||||
- Fix CVE-2026-0994: nested Any messages bypassing recursion depth limits
|
||||
|
||||
* Tue Oct 22 2024 Adrian Reber <areber@redhat.com> - 3.14.0-16
|
||||
- Rebuild
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user