MBM7's picture
Upload 2 files
d6adf2e verified
|
Raw
History Blame Contribute Delete
2.47 kB
---
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 |