Skip to content

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

@thosuperman

Description

@thosuperman

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:

@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:

@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:

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

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

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:

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:

_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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions