protobuf icon indicating copy to clipboard operation
protobuf copied to clipboard

Any JSON recursion depth bypass in Python json_format.ParseDict

Open 34selen opened this issue 2 weeks ago • 2 comments

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


  1. json_format.ParseDict() is called
  2. ConvertMessage() increments recursion_depth
  3. If recursion_depth > max_recursion_depth, a ParseError is raised
  4. 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

34selen avatar Dec 30 '25 11:12 34selen