GHSA-GQVG-GMMX-X4HM
Vulnerability from github – Published: 2026-09-01 17:04 – Updated: 2026-09-01 17:04Summary
MLflow introduced MLFLOW_ALLOW_PICKLE_DESERIALIZATION as a security control to prevent unsafe pickle.load execution during model loading, in response to CVE-2024-37052 through CVE-2024-37060. When set to False, operators expect all pickle deserialization to be blocked. The most recent related fix (#21188) patched a bypass in the pyfunc flavor.
However, the mlflow.statsmodels flavor completely omits this guard. An attacker who places a crafted MLmodel artifact into any accessible artifact store can trigger arbitrary code execution on any process that calls mlflow.pyfunc.load_model() against the malicious model — even when MLFLOW_ALLOW_PICKLE_DESERIALIZATION=False.
This is a security control bypass. The operator believes pickle RCE is mitigated; the statsmodels flavor silently ignores the control.
Root Cause
mlflow.pyfunc.load_model() dispatches to flavor _load_pyfunc implementations via:
# mlflow/pyfunc/__init__.py L1170-1172
model_impl = importlib.import_module(conf[MAIN])._load_pyfunc(data_path)
The guarded pattern (from mlflow/sklearn/__init__.py L526-533, the reference implementation) is:
if (
not MLFLOW_ALLOW_PICKLE_DESERIALIZATION.get()
and not is_in_databricks_runtime()
and not is_in_databricks_model_serving_environment()
):
raise MlflowException("Deserializing model using pickle is disallowed...")
mlflow/statsmodels/__init__.py has no such check:
# L307-320 — no guard anywhere in this file
def _load_model(path):
import statsmodels.iolib.api as smio
return smio.load_pickle(path) # calls pickle.load() directly
def _load_pyfunc(path):
return _StatsmodelsModelWrapper(_load_model(path))
statsmodels.iolib.api.load_pickle is a thin wrapper around pickle.load. Its own docstring warns: "Never unpickle data received from an untrusted or unauthenticated source."
Trigger
An attacker crafts an MLmodel YAML that specifies mlflow.statsmodels as the loader module:
flavors:
python_function:
loader_module: mlflow.statsmodels
data: model.pkl
statsmodels:
data: model.pkl
statsmodels_version: 0.14.0
With a malicious model.pkl placed alongside it in the artifact store, any call to:
os.environ["MLFLOW_ALLOW_PICKLE_DESERIALIZATION"] = "False"
mlflow.pyfunc.load_model("models:/MaliciousModel/1")
...deserializes the pickle file with no guard check, executing arbitrary code with the privileges of the calling process.
On default MLflow deployments (no --app-name basic-auth), authentication is disabled, so artifact upload requires no credentials.
Affected Code
mlflow/statsmodels/__init__.pyL307-310:_load_model— callssmio.load_picklewithout checkingMLFLOW_ALLOW_PICKLE_DESERIALIZATIONmlflow/statsmodels/__init__.pyL313-320:_load_pyfunc— dispatches to_load_modelwithout checking the control
Permalink (commit 0b0c576c):
- https://github.com/mlflow/mlflow/blob/0b0c576c642b5b0d9496c829809c7d097403bc9f/mlflow/statsmodels/init.py#L307-L310
- https://github.com/mlflow/mlflow/blob/0b0c576c642b5b0d9496c829809c7d097403bc9f/mlflow/statsmodels/init.py#L313-L320
Recommended Fix
Add the missing guard to mlflow/statsmodels/__init__.py:
from mlflow.environment_variables import MLFLOW_ALLOW_PICKLE_DESERIALIZATION
from mlflow.utils.databricks_utils import (
is_in_databricks_model_serving_environment,
is_in_databricks_runtime,
)
def _load_model(path):
if (
not MLFLOW_ALLOW_PICKLE_DESERIALIZATION.get()
and not is_in_databricks_runtime()
and not is_in_databricks_model_serving_environment()
):
raise MlflowException(
"Deserializing model using pickle is disallowed, but this statsmodels "
"model requires pickle deserialization. Set environment variable "
"'MLFLOW_ALLOW_PICKLE_DESERIALIZATION' to 'true' to allow this."
)
import statsmodels.iolib.api as smio
return smio.load_pickle(path)
{
"affected": [
{
"package": {
"ecosystem": "PyPI",
"name": "mlflow"
},
"ranges": [
{
"events": [
{
"introduced": "2.1.0"
},
{
"fixed": "3.15.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-502"
],
"github_reviewed": true,
"github_reviewed_at": "2026-09-01T17:04:30Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"details": "## Summary\n\nMLflow introduced `MLFLOW_ALLOW_PICKLE_DESERIALIZATION` as a security control to prevent unsafe `pickle.load` execution during model loading, in response to CVE-2024-37052 through CVE-2024-37060. When set to `False`, operators expect all pickle deserialization to be blocked. The most recent related fix (#21188) patched a bypass in the pyfunc flavor.\n\nHowever, the `mlflow.statsmodels` flavor completely omits this guard. An attacker who places a crafted MLmodel artifact into any accessible artifact store can trigger arbitrary code execution on any process that calls `mlflow.pyfunc.load_model()` against the malicious model \u2014 **even when `MLFLOW_ALLOW_PICKLE_DESERIALIZATION=False`**.\n\nThis is a security control bypass. The operator believes pickle RCE is mitigated; the statsmodels flavor silently ignores the control.\n\n---\n\n## Root Cause\n\n`mlflow.pyfunc.load_model()` dispatches to flavor `_load_pyfunc` implementations via:\n\n```\n# mlflow/pyfunc/__init__.py L1170-1172\nmodel_impl = importlib.import_module(conf[MAIN])._load_pyfunc(data_path)\n```\n\nThe guarded pattern (from `mlflow/sklearn/__init__.py` L526-533, the reference implementation) is:\n\n```\nif (\n not MLFLOW_ALLOW_PICKLE_DESERIALIZATION.get()\n and not is_in_databricks_runtime()\n and not is_in_databricks_model_serving_environment()\n):\n raise MlflowException(\"Deserializing model using pickle is disallowed...\")\n```\n\n`mlflow/statsmodels/__init__.py` has **no such check**:\n\n```\n# L307-320 \u2014 no guard anywhere in this file\ndef _load_model(path):\n import statsmodels.iolib.api as smio\n return smio.load_pickle(path) # calls pickle.load() directly\n\ndef _load_pyfunc(path):\n return _StatsmodelsModelWrapper(_load_model(path))\n```\n\n`statsmodels.iolib.api.load_pickle` is a thin wrapper around `pickle.load`. Its own docstring warns: *\"Never unpickle data received from an untrusted or unauthenticated source.\"*\n\n---\n\n## Trigger\n\nAn attacker crafts an MLmodel YAML that specifies `mlflow.statsmodels` as the loader module:\n\n```\nflavors:\n python_function:\n loader_module: mlflow.statsmodels\n data: model.pkl\n statsmodels:\n data: model.pkl\n statsmodels_version: 0.14.0\n```\n\nWith a malicious `model.pkl` placed alongside it in the artifact store, any call to:\n\n```\nos.environ[\"MLFLOW_ALLOW_PICKLE_DESERIALIZATION\"] = \"False\"\nmlflow.pyfunc.load_model(\"models:/MaliciousModel/1\")\n```\n\n...deserializes the pickle file with **no guard check**, executing arbitrary code with the privileges of the calling process.\n\nOn default MLflow deployments (no `--app-name basic-auth`), authentication is disabled, so artifact upload requires no credentials.\n\n---\n\n## Affected Code\n\n- `mlflow/statsmodels/__init__.py` L307-310: `_load_model` \u2014 calls `smio.load_pickle` without checking `MLFLOW_ALLOW_PICKLE_DESERIALIZATION`\n- `mlflow/statsmodels/__init__.py` L313-320: `_load_pyfunc` \u2014 dispatches to `_load_model` without checking the control\n\nPermalink (commit `0b0c576c`):\n- https://github.com/mlflow/mlflow/blob/0b0c576c642b5b0d9496c829809c7d097403bc9f/mlflow/statsmodels/__init__.py#L307-L310\n- https://github.com/mlflow/mlflow/blob/0b0c576c642b5b0d9496c829809c7d097403bc9f/mlflow/statsmodels/__init__.py#L313-L320\n\n---\n\n## Recommended Fix\n\nAdd the missing guard to `mlflow/statsmodels/__init__.py`:\n\n```\nfrom mlflow.environment_variables import MLFLOW_ALLOW_PICKLE_DESERIALIZATION\nfrom mlflow.utils.databricks_utils import (\n is_in_databricks_model_serving_environment,\n is_in_databricks_runtime,\n)\n\ndef _load_model(path):\n if (\n not MLFLOW_ALLOW_PICKLE_DESERIALIZATION.get()\n and not is_in_databricks_runtime()\n and not is_in_databricks_model_serving_environment()\n ):\n raise MlflowException(\n \"Deserializing model using pickle is disallowed, but this statsmodels \"\n \"model requires pickle deserialization. Set environment variable \"\n \"\u0027MLFLOW_ALLOW_PICKLE_DESERIALIZATION\u0027 to \u0027true\u0027 to allow this.\"\n )\n import statsmodels.iolib.api as smio\n return smio.load_pickle(path)\n```",
"id": "GHSA-gqvg-gmmx-x4hm",
"modified": "2026-09-01T17:04:30Z",
"published": "2026-09-01T17:04:30Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/mlflow/mlflow/security/advisories/GHSA-gqvg-gmmx-x4hm"
},
{
"type": "WEB",
"url": "https://github.com/mlflow/mlflow/pull/24686"
},
{
"type": "WEB",
"url": "https://github.com/mlflow/mlflow/commit/38615289094a4b700a20b5d1dbfbe57bdfb0411f"
},
{
"type": "PACKAGE",
"url": "https://github.com/mlflow/mlflow"
},
{
"type": "WEB",
"url": "https://github.com/mlflow/mlflow/releases/tag/v3.15.0"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H",
"type": "CVSS_V3"
}
],
"summary": "MLFLOW_ALLOW_PICKLE_DESERIALIZATION=False safety control bypassed by mlflow.statsmodels flavor \u2014 RCE via crafted model artifact"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.