googleapis / googleapis/google-api-python-client
Unsafe deserialization in MediaUpload.new_from_json() allows arbitrary code execution via crafted JSON
- Lenguaje dominante
- Python
- Estrellas
- 8.9k
- Forks
- 2.6k
- Merge medio
- 2 d 28 min
- PR fusionados (30 d)
- 17
Descripción
Potential Unsafe Deserialization via Dynamic Import in `MediaUpload.new_from_json()`
## Summary
`MediaUpload.new_from_json()` in `googleapiclient/http.py` performs dynamic module import and class resolution using attacker-controlled `_module` and `_class` fields from JSON input.
If an application deserializes untrusted JSON using this method — either directly or indirectly through `HttpRequest.from_json()` — an attacker may be able to instantiate arbitrary classes and potentially achieve code execution depending on the installed Python packages and available `from_json()` implementations.
This resembles a classic unsafe deserialization pattern (CWE-502: Deserialization of Untrusted Data).
---
# Affected Code
## File
`googleapiclient/http.py`
---
## `MediaUpload.new_from_json()`
Approximate lines 408–425:
```python
@classmethod
def new_from_json(cls, s):
"""Utility class method to instantiate a MediaUpload subclass from a JSON
representation produced by to_json().
Args:
s: string, JSON from to_json().
Returns:
An instance of the subclass of MediaUpload that was serialized with
to_json().
"""
data = json.loads(s)
# Find and call the right classmethod from_json() to restore the object.
module = data["_module"]
m = __import__(module, fromlist=module.split(".")[:-1])
kls = getattr(m, data["_class"])
from_json = getattr(kls, "from_json")
return from_json(s)
```
---
## Indirect Invocation via `HttpRequest.from_json()`
Approximate line 1144:
```python
@staticmethod
def from_json(s, http, postproc):
d = json.loads(s)
if d["resumable"] is not None:
d["resumable"] = MediaUpload.new_from_json(d["resumable"])
return HttpRequest(
http,
postproc,
uri=d["uri"],
...
)
```
---
# Technical Root Cause
The deserialization logic trusts attacker-controlled metadata fields:
- `_module`
- `_class`
These fields are used directly for:
1. Dynamic module import via `__import__()`
2. Arbitrary attribute lookup via `getattr()`
3. Invocation of dynamically resolved `from_json()` methods
No validation, allowlist enforcement, or subclass verification exists.
The vulnerable flow is:
```python
data = json.loads(s)
module = data["_module"] # attacker-controlled
m = __import__(module, ...)
kls = getattr(m, data["_class"]) # attacker-controlled
from_json = getattr(kls, "from_json")
return from_json(s)
```
This creates a generic gadget-style unsafe deserialization primitive.
---
# Security Impact
Potential impacts include:
- Arbitrary module import
- Arbitrary class resolution
- Invocation of unintended `from_json()` methods
- Unsafe object construction
- Potential arbitrary code execution depending on installed packages
The practical exploitability depends heavily on:
- available installed packages
- presence of dangerous gadget classes
- application usage patterns
- attacker ability to modify serialized state
---
# Proof of Concept
## Minimal PoC — Arbitrary Module Import
```python
from googleapiclient.http import MediaUpload
import json
payload = json.dumps({
"_module": "os",
"_class": "path"
})
try:
MediaUpload.new_from_json(payload)
except AttributeError as e:
print(f"Module was imported, failed at: {e}")
```
### Example Output
```text
Module was imported, failed at: module 'posixpath' has no attribute 'from_json'
```
This demonstrates that attacker-controlled modules are imported successfully.
---
# Potential Gadget-Based Exploitation
If an installed package exposes a dangerous `from_json()` implementation:
```python
payload = json.dumps({
"_module": "some_installed_package.dangerous_module",
"_class": "DangerousClass",
"cmd": "..."
})
MediaUpload.new_from_json(payload)
```
A vulnerable gadget class could potentially:
- execute subprocesses
- perform file operations
- trigger network requests
- deserialize nested attacker-controlled content
---
# Attack Scenario
A realistic exploitation scenario requires an application that:
1. Uses `MediaUpload.to_json()` or `HttpRequest.to_json()`
2. Stores serialized state somewhere attacker-modifiable
3. Later restores objects using `from_json()`
Potential examples:
- resumable upload persistence
- task queue serialization
- distributed workers
- Redis-backed state storage
- cached upload sessions
- database-stored request objects
---
## Mitigating Factors
Several factors reduce practical exploitability:
- most applications do not serialize `HttpRequest` objects
- the API appears primarily intended for internal/test workflows
- exploitation requires a compatible gadget class
- no direct remote attack path exists in the library itself
---
# Severity Assessment
## Proposed Classification
- CWE-502: Deserialization of Untrusted Data
---
# Suggested Fix
Restrict deserialization to known safe classes.
Example:
```python
_ALLOWED_MEDIA_UPLOAD_CLASSES = {
("googleapiclient.http", "MediaFileUpload"),
("googleapiclient.http", "MediaIoBaseUpload"),
("googleapiclient.http", "MediaInMemoryUpload"),
}
@classmethod
def new_from_json(cls, s):
data = json.loads(s)
module = data["_module"]
class_name = data["_class"]
if (module, class_name) not in _ALLOWED_MEDIA_UPLOAD_CLASSES:
raise ValueError(
f"Refusing to deserialize untrusted class: "
f"{module}.{class_name}"
)
m = __import__(module, fromlist=module.split(".")[:-1])
kls = getattr(m, class_name)
from_json = getattr(kls, "from_json")
return from_json(s)
```
# Environment
- Package: `google-api-python-client`
- Affected file: `googleapiclient/http.py`
- Affected methods:
- `MediaUpload.new_from_json()`
- `HttpRequest.from_json()`
- Python versions: all supported versions
Guía de contribución
Evaluación
Este issue todavía no se ha evaluado.