// threat brief
MLflow statsmodels flavor ignores the pickle safety switch: loading a crafted model can run arbitrary code
- CVSS
- 8.8 HIGH
- CISA KEV
- Not listed
- Weakness
- CWE-502
- Topics
- AI security
AI review 2026-09-30 (not human-reviewed)Command tests: none yetReproduced: not yetVerification record ↓
MLflow's statsmodels flavor ignores MLFLOW_ALLOW_PICKLE_DESERIALIZATION=False, so teams that blocked pickle can still run attacker code from a tampered model.
| Who is affected | Organizations on MLflow 2.1.0 through 3.14.0 that set MLFLOW_ALLOW_PICKLE_DESERIALIZATION=False expecting MLflow to refuse pickle-based models, and that load models from a store others can write to; in these versions the statsmodels flavor ignores the setting, whether loaded through mlflow.pyfunc.load_model() or mlflow.statsmodels.load_model(). If you never set the variable, MLflow's default (True) already loads pickle by design; that is not caused by this flaw, but the risk of loading untrusted models is there regardless. |
|---|---|
| What happens | If an attacker can place a model where you get models from and your code loads it, the attacker's code runs with the privileges of the loading process, even if you set MLFLOW_ALLOW_PICKLE_DESERIALIZATION=False. |
| What to do now | Upgrade MLflow to 3.15.0 or later (CERT/CC says to upgrade immediately). PlainCVE also suggests setting MLFLOW_ALLOW_PICKLE_DESERIALIZATION to False (neither MLflow nor CERT/CC recommends this): the variable defaults to True, and without setting it MLflow still loads pickle after upgrading; once set, other flavors that need pickle and have this check will also refuse to load. Per fix PR #24686, the check does not run in the Databricks runtime or model serving environments. On any version, load models only from trusted sources. |
| Affected versions | 2.1.0 through 3.14.0 (CVE description); MLflow's GHSA lists 2.1.0 and later as affected with 3.15.0 patched, and the CVE record's structured fields list 2.1 up to, but not including, 3.15.0. Only environments that set MLFLOW_ALLOW_PICKLE_DESERIALIZATION=False are affected |
| Fixed versions | 3.15.0 or later |
| CVSS vector | Show full vectorCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H |
| Timeline |
|
Blue team playbook
Check, fix / mitigate, respond, harden. Matching the affected versions does not mean you were compromised, and a check that finds nothing does not prove you are safe; each item says what it can and cannot show. Tagged steps point to where the attack flow below can be stopped.
- 01Check your MLflow version
2.1.0 through 3.14.0 are affected (MLflow's GHSA lists 3.15.0 as patched). Also consider whether you load models from a store others can write to.
Read-only checkpip show mlflow | grep -i '^version' - 02Check whether the pickle-blocking setting is in effect
This flaw only affects environments that set MLFLOW_ALLOW_PICKLE_DESERIALIZATION to False; in MLflow's source code the variable defaults to True. Run this in the same Python environment and with the same environment variables as the process that actually loads models; only an output of False means pickle loading is forbidden.
Read-only checkpython -c "from mlflow.environment_variables import MLFLOW_ALLOW_PICKLE_DESERIALIZATION as v; print(v.get())" - 03Find models that name the statsmodels flavor
If your team does not normally use statsmodels, models whose MLmodel file names mlflow.statsmodels as the loader are worth flagging for review. This is PlainCVE's suggestion, not MLflow guidance; the command only works for artifact stores on a local file system.
Read-only checkgrep -rl "mlflow.statsmodels" /path/to/artifact-store --include=MLmodel
- 01Upgrade to 3.15.0 or later
MLflow's official 3.15.0 release notes include the change adding the pickle guard to the statsmodels flavor (PR #24686). Per the v3.15.0 source code, the new check blocks only when MLFLOW_ALLOW_PICKLE_DESERIALIZATION is False and the process is not in the Databricks runtime or a Databricks model serving environment. Upgrading alone does not change the default behavior; blocking pickle requires the next item (PlainCVE's suggestion).
Changes your environmentpip install -U "mlflow>=3.15.0" - 02After upgrading, set MLFLOW_ALLOW_PICKLE_DESERIALIZATION to False (PlainCVE's suggestion)
The variable defaults to True (v3.15.0 source code), meaning pickle loading is allowed by default; only when it is False does the statsmodels flavor in 3.15.0 and later refuse pickle models. Set it in the environment of the processes that actually load models (serving, batch jobs, notebooks); the command below only applies to the current shell session. Once set, other flavors that need pickle and have this check will also refuse to load; the workaround MLflow's error message offers is setting it back to true. This is PlainCVE's suggestion: neither MLflow nor CERT/CC recommends that users set it, and fix PR #24686 protects users who have already set it to False.
Configuration exampleexport MLFLOW_ALLOW_PICKLE_DESERIALIZATION=False - 03Can't upgrade yet: accept models only from trusted sources
CERT/CC says the attack requires write access to a place users get models from, so allowing only trusted sources to write or register models until you patch lowers the risk (PlainCVE's suggestion based on that precondition). Note: on older versions, setting MLFLOW_ALLOW_PICKLE_DESERIALIZATION=False alone does not stop this. The sources give no other workaround for the statsmodels flaw itself.
- 01If a suspicious model was loaded, treat the host as compromised
The code runs with the loading process's privileges, so any credentials and data that process could reach may be exposed. This is PlainCVE's suggestion; the sources give no incident response guidance.
- 01Turn on authentication for the MLflow server
According to the GHSA text, default deployments without the basic-auth app allow uploads with no credentials. Turning on login and limiting write access is PlainCVE's suggestion.
- 02Load models only from trusted sources
In environments that have not set MLFLOW_ALLOW_PICKLE_DESERIALIZATION=False, MLflow loads pickle by default, so loading an untrusted model can already run code hidden in it, and upgrading does not change that. The setting is also not yet applied to every flavor: CERT/CC says the dspy flavor has a similar bypass, and only a later MLflow pull request (#25566, merged 2026-09-03) added the same protection to the mlflow.shap flavor. This is PlainCVE's suggestion.
- 03Run model loaders with least privilege
If a malicious model does get loaded, the damage stays limited. This is PlainCVE's suggestion.
Full remediation steps and notes
- Upgrade.
pip install -U "mlflow>=3.15.0"in every environment that loads models (serving, batch jobs, notebooks), not only the tracking server. - After upgrading, set
MLFLOW_ALLOW_PICKLE_DESERIALIZATIONto False (PlainCVE’s suggestion). Neither MLflow nor CERT/CC recommends that users set it; the fix protects users who have already set it to False. Once set, other flavors that need pickle and have this check will also refuse to load. The variable defaults to True, and the check added by the fix, PR #24686, only blocks when it is False; without setting it, the statsmodels flavor still loads pickle after upgrading. Set it in the environment of the processes that actually load models. Per PR #24686, the check is skipped in the Databricks runtime and Databricks model serving environments. On older versions, setting the variable alone does not stop the statsmodels flavor. - Load models only from trusted sources. This is the baseline on any version and setting; where pickle is not forbidden, loading an untrusted model can already run code hidden in it. The sources give no workaround for the statsmodels flaw itself; CERT/CC says to upgrade immediately. The CERT/CC advice to avoid the dspy flavor covers a different dspy flaw and does not help here. CERT/CC says the attack requires write access to a place you get models from, so allowing only trusted sources to write or register models lowers the risk (PlainCVE’s suggestion).
Attack flow
Read left to right: this is the order the attack happens in. A blue shield means the step can be stopped; the earlier, the better. Click an icon for details.
Read top to bottom: this is the order the attack happens in. A blue shield means the step can be stopped; the earlier, the better. Tap an icon for details.
- Attacker
The attacker places a tampered model in a model store
This requires write access to a place users get MLflow models from (such as an artifact store, the storage that holds model files). According to the GHSA text, default MLflow deployments have login switched off, so uploads need no credentials.
Defense: Allow only trusted sources to write or register models; turn on authentication for the MLflow server.
- Attacker
The model's config file names the statsmodels flavor as its loader
MLmodel is the config file shipped with every model; it says which format (flavor) should be used to load it.
Defense: Flag models in the store that name mlflow.statsmodels when your team does not normally use that flavor.
- Your system
The victim's process loads the model with mlflow.pyfunc.load_model()
For example, a model-serving service or batch inference job picks up the model. According to the MLflow 3.14.0 source code, calling mlflow.statsmodels.load_model() directly reaches the same _load_model().
- Your system
The statsmodels _load_model() never checks the pickle-blocking setting
The statsmodels flavor saves models with pickle internally. MLflow allows pickle loading by default; operators can forbid it with MLFLOW_ALLOW_PICKLE_DESERIALIZATION=False, but older versions of the statsmodels flavor ignore that setting and silently load it anyway.
Defense: Upgrade to 3.15.0 or later; PlainCVE also suggests setting MLFLOW_ALLOW_PICKLE_DESERIALIZATION to False (it defaults to True, which blocks nothing); per fix PR
- Attacker
The attacker's code runs with the loading process's privileges
What it can do depends on the data, keys and systems that process can reach.
Defense: Run model-loading processes with least privilege.
Who is affected
| Condition | Affected? |
|---|---|
MLflow 2.1.0 through 3.14.0 with MLFLOW_ALLOW_PICKLE_DESERIALIZATION=False set, loading models from a store others can write to (mlflow.pyfunc.load_model() or mlflow.statsmodels.load_model()) | Affected: the statsmodels flavor ignores the setting |
MLflow 3.15.0 or later with MLFLOW_ALLOW_PICKLE_DESERIALIZATION=False set | Fixed (the check is skipped in the Databricks runtime and model serving environments) |
| Any version with the variable unset (default True) or set to True | Not this flaw: MLflow loads pickle by design, so loading an untrusted model can run code hidden in it |
| MLflow before 2.1.0 | MLflow’s GHSA and the CVE record both start the affected range at 2.1.0 |
At its core this flaw bypasses a setting users turn on themselves. According to MLflow’s source code, MLFLOW_ALLOW_PICKLE_DESERIALIZATION defaults to True, meaning pickle loading is allowed by default; the CVE record, CERT/CC and MLflow’s GHSA all describe the statsmodels flavor loading pickle anyway after users set it to False.
The range is worded slightly differently: the CVE description says “2.1.0 to 3.14.0”; the CVE record’s structured fields say from 2.1 up to, but not including, 3.15.0; MLflow’s GHSA says 2.1.0 and later are affected, with 3.15.0 patched. The CERT/CC note says the flaw was confirmed against MLflow 3.12.0.
An attack generally needs all of these: you have set MLFLOW_ALLOW_PICKLE_DESERIALIZATION=False; the attacker can write to a place you get models from (such as an artifact store, the storage that holds model files); the model’s MLmodel config file names the statsmodels flavor as its loader; and your code loads it. The CERT/CC note describes the attack path through mlflow.pyfunc.load_model(); according to the MLflow 3.14.0 source code (mlflow/statsmodels/init.py), both _load_pyfunc(), which pyfunc loading calls, and a direct call to mlflow.statsmodels.load_model() reach the same unchecked _load_model(), so both ways of loading are affected. According to the GHSA text, default MLflow deployments without the basic-auth app have login switched off, so uploads need no credentials.
How it works
MLflow uses “flavors” to save and load models built with different libraries. According to MLflow’s documentation, the statsmodels flavor uses pickle internally to save models. Pickle is Python’s format for saving objects, and loading an untrusted pickle file can run code hidden inside it. This class of problem is called unsafe deserialization (CWE-502).
For this reason MLflow offers an environment variable (a setting a program reads at startup), MLFLOW_ALLOW_PICKLE_DESERIALIZATION. It defaults to True, which allows pickle loading; setting it to False forbids it. The problem is that the statsmodels flavor’s _load_model() never checked this setting at all. Operators believed pickle risk was blocked, but this flavor silently loaded pickle anyway. The result: a remote attacker who supplies a crafted model may get arbitrary code to run, with the privileges of the loading process.
The flawed approach (conceptual illustration, not the original source code):
Show code example(python)
def _load_model(path):
# Never checks the safety switch; reads the pickle directly
return read_pickle(path)The fixed approach:
Show code example(python)
def _load_model(path):
if not pickle_deserialization_allowed():
raise SecurityError("pickle loading is disabled")
return read_pickle(path)Timeline
| Date | Event |
|---|---|
| 2026-07-27 | MLflow publishes security advisory GHSA-gqvg-gmmx-x4hm (listed as a reference in CERT/CC note VU#369093), the earliest public record of this flaw |
| 2026-07-31 | MLflow v3.15.0 released with the fix (PR #24686). Date per the GitHub releases API and PyPI upload records |
| 2026-09-01 | OSV publishes GHSA-gqvg-gmmx-x4hm |
| 2026-09-16 | CERT/CC publishes note VU#369093 |
| 2026-09-23 | CVE published; CISA’s assessment records no known exploitation |
| 2026-09-25 | Not in CISA KEV as of this date |
Further reading
- The CVE mapping is disputed. The original CERT/CC note VU#369093 (not only a republished copy) describes the dspy flaw first and the statsmodels flaw second, then says they are CVE-2026-96804 and CVE-2026-96775 “respectively”, which read literally makes this CVE the dspy flaw. The official CVE record maps CVE-2026-96804 to statsmodels, and Strix lists CVE-2026-96775 as the dspy flaw. CERT/CC is also the CNA that published this CVE, so its note and its own CVE record disagree. This page follows the CVE record in mapping CVE-2026-96804 to statsmodels.
- The link to MLflow’s advisory. CERT/CC note VU#369093 lists MLflow’s security advisory GHSA-gqvg-gmmx-x4hm as a reference, and that advisory describes exactly this statsmodels pickle-control bypass. The CVE’s own GitHub entry is a separate, unreviewed advisory, GHSA-2qmw-5xhm-qq5w. This page uses the earlier GHSA-gqvg-gmmx-x4hm publication date, 2026-07-27, as the first disclosure date; the CVE was published on 2026-09-23.
- Scoring differences. In the CVE record, CISA rates it 8.8 with a vector assuming low privileges and no user action; GHSA (on OSV and GitLab) also gives 8.8 but assumes no privileges and user action. NVD’s status is “Deferred”, so NVD has done no analysis of its own.
- Exploitation. Not in CISA KEV; CISA’s assessment says no known exploitation, not automatable, total technical impact. On a public proof-of-concept, TheHackerWire says none is confirmed; another third-party site claims one exists, but its page contains runnable attack code, so this page does not cite it. No source offers evidence on exploitation before a fix existed.
- Coordination. The CERT/CC note says MLflow could not be reached to coordinate these vulnerabilities.
- Related flaws. CERT/CC says a similar bypass in the dspy flavor had no fix yet. A later MLflow pull request, #25566 (merged 2026-09-03), used the statsmodels guard as the pattern for adding the same protection to the mlflow.shap flavor; that PR changes only the shap flavor.
Verification Checked item by item against sources by AI under our review rules, all passed; not human-reviewed and not yet reproduced in our lab
| AI review | 2026-09-30 · review rules |
|---|
Verification records describe the environment and the result only, never reproduction steps or code that could attack other people’s systems. See our policy.
Sources
- CVE / NVD / OSVCVE-2026-96804 Record (CVE API) · CVE Program, 2026-09-23 · accessed 2026-09-25
- CVE / NVD / OSVNVD API - CVE-2026-96804 · NIST · accessed 2026-09-25
- AdvisoryGHSA-gqvg-gmmx-x4hm: MLFLOW_ALLOW_PICKLE_DESERIALIZATION=False safety control bypassed by mlflow.statsmodels flavor · MLflow (GitHub), 2026-07-27 · accessed 2026-09-28
- Patch / releaseMLflow v3.15.0 release · MLflow (GitHub), 2026-07-31 · accessed 2026-09-25
- Patch / releaseMLflow pull request #24686 · MLflow (GitHub), 2026-07-27 · accessed 2026-09-28The fix; the new check does not run in the Databricks runtime or Databricks model serving environments
- Othermlflow 3.15.0 (PyPI JSON API) · PyPI, 2026-07-31 · accessed 2026-09-28
- Patch / releaseMLflow pull request #25566 · MLflow (GitHub), 2026-09-03 · accessed 2026-09-28
- Vendormlflow.statsmodels documentation · MLflow · accessed 2026-09-25
- AdvisoryGHSA-gqvg-gmmx-x4hm (GitLab Advisory Database) · GitLab · accessed 2026-09-25
- AdvisoryGHSA-gqvg-gmmx-x4hm (OSV) · OSV, 2026-09-01 · accessed 2026-09-25
- AdvisoryVU#369093: MLflow dspy and statsmodels flavors bypass pickle deserialization control · CERT/CC, 2026-09-16 · accessed 2026-09-28
- Vendormlflow/statsmodels/__init__.py (v3.14.0) · MLflow (GitHub) · accessed 2026-09-28v3.14.0 source: both load_model() and _load_pyfunc() call _load_model()
- Vendormlflow/environment_variables.py (v3.15.0) · MLflow (GitHub) · accessed 2026-09-28MLFLOW_ALLOW_PICKLE_DESERIALIZATION is a boolean environment variable with a default of True (also True in v3.14.0)
- Vendormlflow/statsmodels/__init__.py (v3.15.0) · MLflow (GitHub) · accessed 2026-09-28v3.15.0 source: _load_model() refuses to load only when MLFLOW_ALLOW_PICKLE_DESERIALIZATION is False and the process is not in the Databricks runtime or model serving environment
- OtherCVE-2026-96804 · Strix · accessed 2026-09-25
- NewsCVE-2026-96804 · TheHackerWire · accessed 2026-09-25
Press brief
In one sentence
MLflow is a widely used open-source machine learning platform. It loads pickle-based models by default, and operators can turn on a safety switch that forbids this, but its statsmodels model format never checked that switch, so even with pickle forbidden a tampered model file could run arbitrary code when loaded. MLflow added the check in version 3.15.0, but it only protects users who set the switch to forbid pickle. As of September 25, 2026, the flaw is not in CISA's Known Exploited Vulnerabilities catalog, and no source reports exploitation in the wild.
Key facts
- Severity
- CVSS 3.1 8.8 (CISA ADP, CVE record) cveawg.mitre.org
- Fixed version
- 3.15.0 github.com
- Affected versions
- 2.1.0 up to, but not including, 3.15.0 (MLflow GHSA) github.com
- MLflow advisory (listed as a reference in the CERT/CC note)
- GHSA-gqvg-gmmx-x4hm, 2026-07-27 github.com
- CVE published
- 2026-09-23 cveawg.mitre.org
Confirmed
- MLflow's statsmodels flavor in 2.1.0 through 3.14.0 does not apply the MLFLOW_ALLOW_PICKLE_DESERIALIZATION=False control on load (CVE record).
- MLflow's official 3.15.0 release notes include the change adding the pickle guard to the statsmodels flavor; per the v3.15.0 source code, the guard blocks only when MLFLOW_ALLOW_PICKLE_DESERIALIZATION is set to False, and the variable defaults to True.
- Not in CISA KEV as of 2026-09-25; CISA's 2026-09-23 assessment records no known exploitation.
Unconfirmed / disputed
- The original CERT/CC note VU#369093, read literally, maps CVE-2026-96804 to the dspy flaw, which disagrees with the CVE record; this page follows the CVE record.
- Sources disagree on required privileges: CISA says low privileges and no user action; GHSA says no privileges but user action.
- Whether a public proof-of-concept exists: TheHackerWire says none is confirmed; another third-party site claims one exists, but its page contains runnable attack code, so this page does not cite it.
- No source offers evidence on whether it was exploited before a fix existed.
Quotable line
Operators thought they had locked the door, but one side entrance never had a lock at all.
Images
Download the share image (PNG) · Attack flow diagram (downloadable)
Please credit "PlainCVE" and link to this page. Full citation format is under "How to cite" below. Found a factual error? Report it.
MLflow statsmodels flavor ignores the pickle safety switch: loading a crafted model can run arbitrary code MLflow is a widely used open-source machine learning platform. It loads pickle-based models by default, and operators can turn on a safety switch that forbids this, but its statsmodels model format never checked that switch, so even with pickle forbidden a tampered model file could run arbitrary code when loaded. MLflow added the check in version 3.15.0, but it only protects users who set the switch to forbid pickle. As of September 25, 2026, the flaw is not in CISA's Known Exploited Vulnerabilities catalog, and no source reports exploitation in the wild. Key facts: - Severity:CVSS 3.1 8.8 (CISA ADP, CVE record)(https://cveawg.mitre.org/api/cve/CVE-2026-96804) - Fixed version:3.15.0(https://github.com/mlflow/mlflow/releases/tag/v3.15.0) - Affected versions:2.1.0 up to, but not including, 3.15.0 (MLflow GHSA)(https://github.com/mlflow/mlflow/security/advisories/GHSA-gqvg-gmmx-x4hm) - MLflow advisory (listed as a reference in the CERT/CC note):GHSA-gqvg-gmmx-x4hm, 2026-07-27(https://github.com/mlflow/mlflow/security/advisories/GHSA-gqvg-gmmx-x4hm) - CVE published:2026-09-23(https://cveawg.mitre.org/api/cve/CVE-2026-96804) Confirmed: - MLflow's statsmodels flavor in 2.1.0 through 3.14.0 does not apply the MLFLOW_ALLOW_PICKLE_DESERIALIZATION=False control on load (CVE record). - MLflow's official 3.15.0 release notes include the change adding the pickle guard to the statsmodels flavor; per the v3.15.0 source code, the guard blocks only when MLFLOW_ALLOW_PICKLE_DESERIALIZATION is set to False, and the variable defaults to True. - Not in CISA KEV as of 2026-09-25; CISA's 2026-09-23 assessment records no known exploitation. Unconfirmed / disputed: - The original CERT/CC note VU#369093, read literally, maps CVE-2026-96804 to the dspy flaw, which disagrees with the CVE record; this page follows the CVE record. - Sources disagree on required privileges: CISA says low privileges and no user action; GHSA says no privileges but user action. - Whether a public proof-of-concept exists: TheHackerWire says none is confirmed; another third-party site claims one exists, but its page contains runnable attack code, so this page does not cite it. - No source offers evidence on whether it was exploited before a fix existed. 「Operators thought they had locked the door, but one side entrance never had a lock at all.」— PlainCVE https://plaincve.date/en/vulns/cve-2026-96804-mlflow-statsmodels-pickle-bypass
How to cite this page
This article is CC BY 4.0. Please keep the attribution and link when republishing.
PlainCVE Team (2026). "MLflow statsmodels flavor ignores the pickle safety switch: loading a crafted model can run arbitrary code". PlainCVE. https://plaincve.date/en/vulns/cve-2026-96804-mlflow-statsmodels-pickle-bypass (accessed YYYY-MM-DD)BibTeX
@misc{cve202696804mlflowstatsmodelspicklebypass2026,
title = {MLflow statsmodels flavor ignores the pickle safety switch: loading a crafted model can run arbitrary code},
author = {PlainCVE Team},
year = {2026},
howpublished = {PlainCVE},
url = {https://plaincve.date/en/vulns/cve-2026-96804-mlflow-statsmodels-pickle-bypass},
note = {Updated 2026-09-30}
}