Skip to content

Any JSON recursion depth bypass in Python json_format.ParseDict #25070

@34selen

Description

@34selen

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

Metadata

Metadata

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions