googleapis / googleapis/google-api-python-client

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

Abierto
#2,748 0 comentarios 0 reacciones 1 asignado Reclamado por @chalmerlowe Ver en GitHub
priority: p2 type: bug
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

Abrir la guía de contribución

Evaluación

Este issue todavía no se ha evaluado.

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.