# MLflow vulnerable to remote code execution on unauthenticated servers via third-party scorer deserialization importing caller-placed artifact modules

MLflow's third-party scorer deserializer imports any module under allowed package prefixes; uploading a Python file into a run artifact dir under that prefix yields unauthenticated RCE.

- Payout: Research
- Severity: critical
- Program: MLflow
- Found by: mskca
- Disclosed: 2026-10-27
- CVE: CVE-2026-116692
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-26p8-2jq9-3vq9
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-26p8-2jq9-3vq9
- Tags: area:ai domain:ai domain:web impact:privesc impact:rce kind:research platform:ghsa sev:critical tech:python technique:source-review vuln:auth-bypass vuln:deserialization vuln:file-upload vuln:rce

**Target:** MLflow tracking server scorer-invoke AJAX endpoint and artifact upload
**Root cause:** The third-party scorer allow-list uses a string prefix check (module_path == m or startswith(m + '.')) rather than a provenance boundary, and the deserializer calls importlib.import_module plus getattr and class instantiation on request-controlled values before any trace validation.
**Steps:**
1. Create experiment with `artifact_location` set to `file:///usr/local/lib/python3.10/site-packages/mlflow/genai/scorers/trulens`
2. Create a run to allocate `<artifact_location>/<run_id>/artifacts/`
3. Upload `audit_<token>.py` into the run artifact directory via artifact-upload endpoint
4. Submit serialized scorer with module path `mlflow.genai.scorers.trulens.<run_id>.artifacts.audit_<token>` and attacker class name
5. POST to `/ajax-api/3.0/mlflow/scorer/invoke` to trigger import and constructor execution
**How found:** Reporter noticed the allow-list is a string prefix check and that MLflow's file-backed artifact layout lets a caller place files beneath an allowed package directory, then traced that deserialization in `_invoke_scorer_handler` occurs before trace validation.
**Exploitation:** Unauthenticated caller on default no-auth server creates experiment pointing artifact_location at a writable allowed scorer package dir, creates run, uploads malicious module, then invokes scorer endpoint; server imports module and instantiates attacker class, executing code as MLflow process (uid 0 in official image). Proof writes marker file retrieved via artifact-download endpoint.
**Escalation:** Stock image runs as uid 0, so code execution yields root within container; host impact depends on container runtime, mounts, capabilities and socket exposure.
**Insight:** When an allow-list is a string prefix over importable module paths, check whether any upload or artifact mechanism can write files beneath those prefixes — a package-name prefix is not a provenance boundary.

## Proof of concept (copied exactly from the original report)

**Remote PoC invocation against target**

```
python3 new_remote_poc.py \
  --target http://<REDACTED_PUBLIC_IP> \
  --package-dir /usr/local/lib/python3.10/site-packages/mlflow/genai/scorers/trulens \
  --acknowledge-owned-target I_OWN_THIS_TARGET
```

**Server command inside disposable container**

```
mlflow server \
  --host 127.0.0.1 \
  --port 5000 \
  --backend-store-uri sqlite:////work/mlflow.db \
  --default-artifact-root /work/artifacts
```

**Local reproduction run command**

```
./run-official-image.sh
```

