googleapis / googleapis/google-api-python-client

Unsafe deserialization in MediaUpload.new_from_json() allows arbitrary code execution via crafted JSON

Đang mở
#2,748 0 bình luận 0 reaction 1 người được giao Được @chalmerlowe nhận Xem trên GitHub
priority: p2 type: bug
Ngôn ngữ chính
Python
Star
8.9k
Fork
2.6k
Merge trung bình
2 ngày 28 phút
Pull request đã merge (30 ngày)
17

Mô tả

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

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Đánh giá

Issue này chưa được đánh giá.

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.