CVE-2026-0994

Recursive Hell: Breaking Python Protobuf with Nested 'Any' Messages

Amit Schendel
Amit Schendel
Senior Security Researcher

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.

  1. It sees the first Any.
  2. It looks inside, sees another Any (a WKT).
  3. It shortcuts the recursion check and calls the handler.
  4. The handler sees another Any.
  5. 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.

Fix Analysis (1)

Technical Appendix

CVSS Score
8.2/ 10
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:L

Affected Systems

Python applications using `google.protobuf` librarygRPC services accepting JSON transcodingData pipelines parsing untrusted Protobuf JSON

Affected Versions Detail

Product
Affected Versions
Fixed Version
protobuf-python
Google
>= 33.0See Vendor Advisory
AttributeDetail
CWECWE-674 (Uncontrolled Recursion)
CVSS v4.08.2 (High)
Attack VectorNetwork
ImpactAvailability (DoS)
Vulnerable Function_ConvertAnyMessage
Exploit StatusPoC Available
CWE-674
Uncontrolled Recursion

The product does not properly control the amount of recursion that takes place, consuming excessive resources, such as memory or the program stack.

Vulnerability Timeline

Vulnerability identified
2026-01-22
Patch submitted (PR #25239)
2026-01-23
CVE-2026-0994 Published
2026-01-23

Subscribe to updates

Get the latest CVE analysis reports delivered to your inbox.