Skip to content

Instantly share code, notes, and snippets.

@nedlir
Created May 19, 2026 07:40
Show Gist options
  • Select an option

  • Save nedlir/11fb77f35a59cbba73392a086b02a9c6 to your computer and use it in GitHub Desktop.

Select an option

Save nedlir/11fb77f35a59cbba73392a086b02a9c6 to your computer and use it in GitHub Desktop.
CVE-2026-31072

Remote Code Execution via Insecure Deserialization in APScheduler JSONSerializer

The APScheduler JSONSerializer (and CBORSerializer) are documented as secure alternatives to Python's pickle, but both implement a custom object reconstruction mechanism that allows for arbitrary class instantiation and state injection. This design flaw enables Remote Code Execution (RCE) via insecure deserialization.

Vulnerability Summary

  • Type: CWE-502 (Deserialization of Untrusted Data)
  • Affected Version: 4.0.0a3, 4.0.0a4, 4.0.0a5, and 4.0.0a6 (all published GitHub releases in the 4.x pre-release line; verified on 4.0.0a6). JSON/CBOR serializers were introduced in 4.0.0a1 per upstream changelog. The 3.x release line (latest stable: 3.11.2) does not include these serializers.
  • Attack Vector: Network (when attacker-controlled serialized data reaches the deserializer)
  • Authentication Required: Depends on deployment

Product Information

Vulnerable Components

  • apscheduler.serializers.json.JSONSerializer
  • apscheduler.serializers.cbor.CBORSerializer

Affected Methods

  • src/apscheduler/serializers/json.py: _object_hook
  • src/apscheduler/_marshalling.py: unmarshal_object

Vulnerable Logic

The serializer uses a custom object_hook to handle types not natively JSON-serializable. When a JSON object contains the key _apscheduler_json, it calls unmarshal_object to reconstruct the Python object:

def _object_hook(self, obj_state: dict[str, Any]):
    if self.magic_key in obj_state:
        ref, state = obj_state[self.magic_key]
        return unmarshal_object(ref, state)  # <--- Vulnerability Entry Point
    return obj_state

The unmarshal_object function performs:

  1. Dynamic Import: Takes a string reference (e.g., "module.path:ClassName") and dynamically imports the module.
  2. Arbitrary Instantiation: Retrieves the class and creates a new instance with cls.__new__(cls).
  3. State Injection: Calls __setstate__ on the instance, passing attacker-controlled data.
def unmarshal_object(ref: str, state: Any) -> Any:
    cls = callable_from_ref(ref)  # <--- Resolves any class from any module
    if not isinstance(cls, type):
        raise TypeError(f"{ref} is not a class")
    instance = cls.__new__(cls)
    instance.__setstate__(state)  # <--- Executes code with attacker-controlled state
    return instance

Impact:

  • Instantiation of any class available in the Python environment (including standard library gadgets or application-specific classes)
  • Invocation of __setstate__ with arbitrary attacker-controlled arguments
  • Remote Code Execution (RCE) in the context of the scheduler process

Example Payload

A malicious payload delivered to the vulnerable deserializer takes the following form:

{
  "_apscheduler_json": [
    "__main__:VulnerableGadget",
    { "cmd": "whoami > pwn.txt" }
  ]
}

When deserialized, the magic key triggers unmarshal_object, which resolves the referenced class, instantiates it via __new__, and invokes __setstate__ with the attacker-controlled dictionary. A gadget class whose __setstate__ passes the cmd value to os.system (or any equivalent sink) results in the scheduler process executing the command — for example, writing the current username to pwn.txt.

References

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment