Safeguard
AI Security•

MLflow's Pickle Kill Switch Skipped the statsmodels Flavor

GHSA-gqvg-gmmx-x4hm: with MLFLOW_ALLOW_PICKLE_DESERIALIZATION=False, a model tagged as statsmodels still unpickled and ran code. Fixed in MLflow 3.15.0.

Safeguard Security Team
5 min read

On 1 September 2026, MLflow published GHSA-gqvg-gmmx-x4hm, rated high (CVSS 8.8). MLflow has an environment variable, MLFLOW_ALLOW_PICKLE_DESERIALIZATION, that operators set to False to stop model loading from calling pickle.load. The mlflow.statsmodels flavor never checked it. A crafted model artifact that names that flavor as its loader runs arbitrary code in whatever process calls mlflow.pyfunc.load_model(), with the kill switch set.

No CVE ID is attached to the advisory. The fix shipped in MLflow 3.15.0, which PyPI shows was uploaded on 31 July 2026.

What's affected

  • Package: mlflow on PyPI.
  • Affected: 2.1.0 up to, but not including, 3.15.0.
  • Fixed: 3.15.0. The current release is 3.17.1.

The advisory lays out the history. The variable was introduced in response to CVE-2024-37052 through CVE-2024-37060, a set of pickle deserialization bugs across MLflow flavors. A previous fix closed a bypass in the pyfunc flavor. The statsmodels flavor's _load_model() still called statsmodels.iolib.api.load_pickle, a thin wrapper around pickle.load, with no guard. We checked the 3.15.0 source: mlflow/statsmodels/__init__.py now raises an MlflowException before loading when the variable is false, matching the sklearn flavor.

The trigger is a two-file artifact: an MLmodel YAML whose python_function flavor sets loader_module: mlflow.statsmodels, and a malicious model.pkl beside it. Loading it with mlflow.pyfunc.load_model("models:/Name/1") deserializes the pickle.

How to check if you're exposed

The first question is whether you ever turned the switch off. In MLflow 3.15.0's environment_variables.py, MLFLOW_ALLOW_PICKLE_DESERIALIZATION defaults to True. So:

  • If you never set it, MLflow loads pickled models by design, from every flavor. This advisory doesn't change your risk, because you weren't relying on the control. Your real exposure is who can write to your artifact store.
  • If you set it to False in serving, batch scoring or CI to block pickle, then any version below 3.15.0 left a hole you believed was closed. That is the population this advisory is about.

Check the setting and version where models are loaded:

env | grep MLFLOW_ALLOW_PICKLE_DESERIALIZATION
pip show mlflow | grep -i ^version
grep -rn "MLFLOW_ALLOW_PICKLE_DESERIALIZATION" deploy/ k8s/ helm/ .github/ 2>/dev/null

Then look for models in your registry that use the statsmodels loader. This reads only the MLmodel metadata, not the pickle:

import mlflow
from mlflow import MlflowClient

client = MlflowClient()
for mv in client.search_model_versions():
    uri = f"models:/{mv.name}/{mv.version}"
    flavors = mlflow.models.get_model_info(uri).flavors
    loader = flavors.get("python_function", {}).get("loader_module")
    if loader == "mlflow.statsmodels":
        print(uri, mv.source)

A statsmodels model you didn't expect, especially one registered by an unusual user or from an unfamiliar run, deserves a look before anything loads it.

Finally, check who can write models. The advisory notes that on default MLflow deployments without --app-name basic-auth, authentication is disabled, so artifact upload requires no credentials.

What to do now

  1. Upgrade every process that loads models (serving containers, batch jobs, notebooks used for evaluation) to MLflow 3.15.0 or later.
  2. Keep MLFLOW_ALLOW_PICKLE_DESERIALIZATION=false where you can, and treat it as one layer, not the boundary.
  3. Turn on authentication for the tracking server. Without it, anyone who can reach the server can register a model.
  4. Restrict write access to the artifact store (S3 bucket, GCS bucket, Azure container) to the identities that train and register models.
  5. Load models from the registry only in sandboxed workers with no cloud credentials beyond what inference needs.
  6. Prefer serialization formats that don't execute code, such as ONNX or safetensors weights, for models that cross trust boundaries.

Why it matters

Pickle is a code execution format, and every ML platform that loads pickled models is running code from whoever wrote the artifact. Kill switches like this one are valuable, but they only work if every loader checks them. One forgotten flavor turned a deliberate security setting into false comfort.

That's the pattern to watch for in your own platform: a central safety flag that each plugin or flavor is supposed to honor individually. When a new loader is added, nothing forces it to check the flag. A test that loads a pickled model through every registered flavor with the flag off, and expects every one to refuse, would have caught this.

Try it yourself

Run the registry script above against a staging tracking server and count how many model versions use each loader module. Then see how a model risk scan inspects model artifacts for unsafe serialization before they're loaded:

Click through it at your own pace, or open it full screen.

Quick check

If I never set MLFLOW_ALLOW_PICKLE_DESERIALIZATION, does this advisory matter?

Less than you might think. The variable defaults to True, so pickled models already load from every flavor. Your exposure is whoever can write models to your registry or artifact store.

Which MLflow version adds the guard to the statsmodels flavor?

3.15.0, uploaded to PyPI on 31 July 2026. The advisory was published on 1 September.

Does loading the model require calling the statsmodels API directly?

No. mlflow.pyfunc.load_model dispatches to the loader module named in the MLmodel file, so a generic pyfunc load triggers it.

Sources

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.