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.
- Type: CWE-502 (Deserialization of Untrusted Data)
- Affected Version:
4.0.0a3,4.0.0a4,4.0.0a5, and4.0.0a6(all published GitHub releases in the 4.x pre-release line; verified on4.0.0a6). JSON/CBOR serializers were introduced in4.0.0a1per 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: APScheduler
- Vendor: Alex Grönholm
- Repository: https://github.com/agronholm/apscheduler
apscheduler.serializers.json.JSONSerializerapscheduler.serializers.cbor.CBORSerializer
src/apscheduler/serializers/json.py:_object_hooksrc/apscheduler/_marshalling.py:unmarshal_object
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_stateThe unmarshal_object function performs:
- Dynamic Import: Takes a string reference (e.g.,
"module.path:ClassName") and dynamically imports the module. - Arbitrary Instantiation: Retrieves the class and creates a new instance with
cls.__new__(cls). - 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 instanceImpact:
- 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
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.