googleapis / googleapis/google-api-python-client

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

Open
#2,748 0 comments 0 reactions 1 assignee Claimed by @chalmerlowe View on GitHub
priority: p2 type: bug
Dominant language
Python
Stars
8.9k
Forks
2.6k
Avg merge
2d 28m
Merged PRs (30d)
17

Description

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

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.