-
Notifications
You must be signed in to change notification settings - Fork 16.1k
Description
Incomplete fix for CVE-2026-0994: Struct/Value/ListValue mutual recursion bypasses max_recursion_depth in Python JSON parser
Summary
The fix for CVE-2026-0994 (GHSA-7gcm-g887-7qv7) patched _ConvertAnyMessage by
routing it through ConvertMessage(), which increments self.recursion_depth.
However, the identical pattern remains unfixed in three other methods:
_ConvertValueMessage, _ConvertStructMessage, and
_ConvertListOrTupleValueMessage. These methods call each other in a mutual
recursion cycle without ever passing through ConvertMessage(), so
self.recursion_depth never advances past 1. The max_recursion_depth
parameter is completely inert for these well-known types.
Confirmed vulnerable: 6.33.5 (the CVE-2026-0994 patched release) and 7.34.0 (latest as of this report).
Root cause
File: python/google/protobuf/json_format.py
ConvertMessage() (line 531) is the sole location that increments
self.recursion_depth. The three WKT conversion methods call each other
directly, bypassing this increment:
_ConvertValueMessage → _ConvertStructMessage (line 778)
_ConvertStructMessage → _ConvertValueMessage (line 818)
_ConvertValueMessage → _ConvertListOrTupleValueMessage (line 780)
_ConvertListOrTupleValueMessage → _ConvertValueMessage (line 804)
The result: a caller that sets max_recursion_depth=10 and parses a
google.protobuf.Struct from untrusted JSON receives no protection.
Parsing succeeds at depth 100, 200, 500, … until Python's own default
recursion limit (~1 000) is hit, at which point the process raises an
unhandled RecursionError.
Reproduction
from google.protobuf import json_format, struct_pb2~2 KB payload, 500 levels deep
nested = {"leaf": "data"}
for _ in range(500):
nested = {"level": nested}msg = struct_pb2.Struct()
max_recursion_depth=10 should reject this immediately.
Instead it crashes with RecursionError.
json_format.ParseDict(nested, msg, max_recursion_depth=10)
Expected: json_format.ParseError raised at depth 10.
Actual: RecursionError (Python stack exhausted, worker process crashes).
All four type variants are affected (see attached PoC for full test suite):
| Type | Depth 100, limit 10 | Depth 500, limit 10 |
|---|---|---|
| Struct | parses — limit bypassed | RecursionError |
| Value | parses — limit bypassed | RecursionError |
| ListValue | parses — limit bypassed | RecursionError |
Proposed fix
Add the same depth guard used by ConvertMessage() to each of the three
offending methods. The try/finally is required so that a mid-parse
exception does not leave the counter permanently incremented.
def _ConvertStructMessage(self, value, message): if self.recursion_depth >= self.max_recursion_depth: raise ParseError( "Message too nested: the value specified for message type " f"{message.DESCRIPTOR.full_name} is too deeply nested." ) self.recursion_depth += 1 try: # ... existing body unchanged ... finally: self.recursion_depth -= 1def _ConvertValueMessage(self, value, message):
if self.recursion_depth >= self.max_recursion_depth:
raise ParseError(
"Message too nested: the value specified for message type "
f"{message.DESCRIPTOR.full_name} is too deeply nested."
)
self.recursion_depth += 1
try:
# ... existing body unchanged ...
finally:
self.recursion_depth -= 1
def _ConvertListOrTupleValueMessage(self, value, message):
if self.recursion_depth >= self.max_recursion_depth:
raise ParseError(
"Message too nested: the value specified for message type "
f"{message.DESCRIPTOR.full_name} is too deeply nested."
)
self.recursion_depth += 1
try:
# ... existing body unchanged ...
finally:
self.recursion_depth -= 1
A ready-to-apply patch is attached to this issue.
Impact
google.protobuf.Struct is the standard representation for arbitrary JSON
in gRPC APIs. Google Cloud services (Dialogflow intents, Firestore documents,
Cloud Functions event payloads) and countless third-party gRPC services
accept Struct fields from untrusted callers.
Any service that:
- accepts a
Struct,Value, orListValuefield from untrusted input, and - calls
ParseDict(orParse) with an explicitmax_recursion_depth
…believes it is protected against deeply-nested payloads. It is not. A single ~2 KB HTTP/gRPC request crashes one Python worker. Sending the same request in a loop achieves full service outage.
References
- CVE-2026-0994 / GHSA-7gcm-g887-7qv7 — original Any bypass
- Fix commit for CVE-2026-0994 (routes
_ConvertAnyMessagethroughConvertMessage) python/google/protobuf/json_format.pylines 775–821
Workaround
Until a patch is released, services can enforce a depth limit at the transport
layer (e.g. validate JSON depth before calling ParseDict) or wrap the call
and catch RecursionError explicitly:
import sys
old_limit = sys.getrecursionlimit()
sys.setrecursionlimit(old_limit // 2) # tighten the safety net
try:
json_format.ParseDict(data, msg, max_recursion_depth=10)
except RecursionError:
raise ValueError("Input too deeply nested")
finally:
sys.setrecursionlimit(old_limit)
Note: this is not a proper fix—it merely converts the crash into a catchable exception. Apply the patch above for a correct solution.