Recursive Hell: Breaking Python Protobuf with Nested 'Any' Messages
Jan 23, 2026·5 min read·7 visits
Executive Summary (TL;DR)
The Python implementation of Protocol Buffers contained a critical oversight in how it parsed 'Well-Known Types' nested inside `google.protobuf.Any` messages. By recursively nesting `Any` messages, an attacker could bypass the `max_recursion_depth` check entirely. This allows a relatively small JSON payload to trigger an infinite recursion loop in the parsing logic, hitting the Python interpreter's stack limit and crashing the application (DoS).
A logic flaw in Google's Python Protobuf implementation allows attackers to bypass recursion limits using nested 'Any' types, leading to a Denial of Service via stack exhaustion.
The Hook: The Russian Doll of Death
Protocol Buffers (Protobuf) are the lingua franca of modern microservices, acting as the efficient, binary glue holding together gRPC architectures. But sometimes, you need flexibility. Enter the google.protobuf.Any type—a polymorphic feature that lets you embed messages without defining their type upfront. It's effectively a void* for your schema.
While powerful, dynamic typing in a serialization format is a notorious minefield. The vulnerability we are looking at today, CVE-2026-0994, is a classic example of what happens when you trust your own "Well-Known Types" (WKT) a little too much.
The concept is simple: developers set a max_recursion_depth to prevent stack overflows. It's the bouncer at the club ensuring things don't get too rowdy. But in version 33.0+, the Protobuf parser inadvertently gave Any messages a VIP pass, allowing them to skip the bouncer entirely. This means an attacker can hand the server a JSON object that looks small but unpacks into a stack-crushing infinite loop.
The Flaw: Skipping the Checkpoint
To understand the bug, you have to look at google/protobuf/json_format.py. When the parser encounters a message, it typically calls ConvertMessage(), a function that dutifully increments a recursion counter, checks if it hits the limit, and then proceeds. So far, so good.
However, the handling for Any messages contained a fatal optimization. When the parser unpacked an Any message, it checked if the inner content was a "Well-Known Type" (like Struct, Duration, or interestingly, another Any). If it was, the code used Python's operator.methodcaller to invoke the specific handler for that type directly.
This direct invocation was the critical failure. By jumping straight to the handler logic, the code completely bypassed the ConvertMessage() gateway. Consequently, the recursion depth counter was never incremented for that nesting level. It was effectively a "free move" in the recursion game. By chaining these free moves together, you could go as deep as you wanted, regardless of the security settings.
The Code: The Smoking Gun
Let's look at the diff. It's a textbook example of how a small helper function usage can undermine security guarantees.
In the vulnerable code, methodcaller is used to dispatch the call. Note the lack of self.ConvertMessage:
# VULNERABLE CODE
elif full_name in _WKTJSONMETHODS:
methodcaller(
_WKTJSONMETHODS[full_name][1],
value['value'],
sub_message,
'{0}.value'.format(path),
)(self)The fix, applied in PR #25239, forces the logic back through the main conversion pipeline. This ensures that every layer of the onion is counted against the quota:
# PATCHED CODE
elif full_name in _WKTJSONMETHODS:
# For well-known types (including nested Any), use ConvertMessage
# to ensure recursion depth is properly tracked
self.ConvertMessage(
value['value'],
sub_message,
'{0}.value'.format(path)
)By routing the call back through self.ConvertMessage, the _recursion_depth check is triggered before the nested content is processed. If _recursion_depth exceeds the limit, it throws a ParseError immediately, rather than letting the Python interpreter crash with a RecursionError later.
The Exploit: Crashing the Interpreter
Exploiting this is trivially easy. We don't need shellcode or ROP chains; we just need JSON. The goal is to construct a nested structure that exceeds the Python interpreter's stack limit (usually 1000 frames) but would theoretically pass the Protobuf parser's default limit (usually 100) because of the bug.
The payload looks like this:
{
"@type": "type.googleapis.com/google.protobuf.Any",
"value": {
"@type": "type.googleapis.com/google.protobuf.Any",
"value": {
"@type": "type.googleapis.com/google.protobuf.Any",
"value": { ... repeat 1000 times ... }
}
}
}When json_format.ParseDict() eats this, it dives deep.
- It sees the first
Any. - It looks inside, sees another
Any(a WKT). - It shortcuts the recursion check and calls the handler.
- The handler sees another
Any. - Goto step 3.
Eventually, CPython screams. A RecursionError is raised. If this exception isn't explicitly caught and handled (and most generic web frameworks won't catch a RecursionError gracefully during data binding), the worker process terminates. Do this continuously, and you have a persistent Denial of Service.
The Impact: Why Panic?
While this is "only" a Denial of Service, the context matters. Python Protobuf is widely used in API gateways, backend workers, and data processing pipelines.
Imagine a public-facing API endpoint that accepts JSON and converts it to Protobuf to talk to backend gRPC services. An attacker can hammer this endpoint with a few kilobytes of JSON data. Each request kills a worker process. If you are running a standard WSGI/ASGI server (like Gunicorn or Uvicorn) with a fixed number of workers, the attacker can starve the entire pool with very low bandwidth.
This isn't just about crashing a script; it's about resource exhaustion. The ease of exploitation (Network vector, Low complexity, No auth) gives it a CVSS score of 8.2 for a reason. It's a cheap, effective way to take down a Python-based microservice architecture.
Official Patches
Fix Analysis (1)
Technical Appendix
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:LAffected Systems
Affected Versions Detail
| Product | Affected Versions | Fixed Version |
|---|---|---|
protobuf-python Google | >= 33.0 | See Vendor Advisory |
| Attribute | Detail |
|---|---|
| CWE | CWE-674 (Uncontrolled Recursion) |
| CVSS v4.0 | 8.2 (High) |
| Attack Vector | Network |
| Impact | Availability (DoS) |
| Vulnerable Function | _ConvertAnyMessage |
| Exploit Status | PoC Available |
MITRE ATT&CK Mapping
The product does not properly control the amount of recursion that takes place, consuming excessive resources, such as memory or the program stack.
Known Exploits & Detection
Vulnerability Timeline
Subscribe to updates
Get the latest CVE analysis reports delivered to your inbox.