| --- |
| tags: |
| - security |
| - vulnerability |
| - poc |
| - protobuf |
| - google |
| - uncontrolled-recursion |
| - cwe-674 |
| - dos |
| license: mit |
| --- |
| |
| # protobuf β json_format.ParseDict() Uncontrolled Recursion (PoC) |
| |
| **Repo:** `MBM7/protobuf-parsedicts-recursion-poc` |
| **Status:** Responsible disclosure β submitted to Huntr |
| **Severity:** High / CWE-674 |
| **Package:** `protobuf` (PyPI) β google.protobuf |
| |
| --- |
| |
| ## Summary |
| |
| A **7.4 KB** crafted JSON payload with 200 nesting levels crashes any Python |
| server calling `ParseDict()` with a `RecursionError` β unhandled, terminates |
| the process. |
| |
| | Payload size | Depth | Result | |
| |-------------|-------|------------------| |
| | 7,443 bytes | 200 | RecursionError β | |
| | 9,293 bytes | 250 | RecursionError β | |
| |
| --- |
| |
| ## Root Cause |
| |
| `google/protobuf/json_format.py`, `_ConvertMessage()`: |
| |
| ```python |
| def _ConvertMessage(value, message, path): |
| for field_name, field_value in value.items(): |
| ... |
| _ConvertMessage(field_value, sub_message, path) # β no depth check |
| ``` |
| |
| `ParseDict()` β `_ConvertMessage()` recurses once per nesting level |
| with **no depth limit**. Compare to the binary parser which has |
| `DEFAULT_RECURSION_LIMIT = 100`. |
|
|
| --- |
|
|
| ## Asymmetry |
|
|
| | Parser | Depth limit | |
| |--------|-------------| |
| | `ParseFromString()` (binary) | `DEFAULT_RECURSION_LIMIT = 100` β
| |
| | `ParseDict()` (JSONβproto) | **NONE** β | |
| | `MessageToJson()` | **NONE** β | |
| | `MessageToDict()` | **NONE** β | |
|
|
| --- |
|
|
| ## Attack scenario |
|
|
| ```python |
| # Server receives JSON from user, converts to proto: |
| user_json = json.loads(request.body) |
| proto_msg = MyMessage() |
| ParseDict(user_json, proto_msg) # β CRASH at depth 200 |
| ``` |
|
|
| A 7.4 KB JSON payload with 200 nesting levels crashes the Python process. |
|
|
| --- |
|
|
| ## Reproduce |
|
|
| ```bash |
| pip install protobuf |
| python poc_protobuf_recursive_json.py |
| ``` |
|
|
| Expected: |
| ``` |
| Nesting depth : 200 levels |
| JSON payload : 7,443 bytes |
| Result : RecursionError β server CRASH β |
| ``` |
|
|
| --- |
|
|
| ## Suggested Fix |
|
|
| ```python |
| def _ConvertMessage(value, message, path, depth=0): |
| if depth > _MAX_RECURSION_DEPTH: # e.g. 100, matching binary parser |
| raise ParseError('Message too deeply nested') |
| for field_name, field_value in value.items(): |
| ... |
| _ConvertMessage(field_value, sub_message, path, depth + 1) |
| ``` |
|
|
| --- |
|
|
| ## Environment |
|
|
| | Package | Version | |
| |----------|---------| |
| | protobuf | 5.29.6 | |
| | Python | 3.12 | |
|
|