MLflow Unauthenticated RCE on Tracking Servers (CVE-2026-116692)
CVE-2026-116692 (CVSS 9.8) lets any caller of a default MLflow 3.12.0-3.16.x tracking server upload and run Python code as the server process; fixed in 3.17.0.
TL;DR
- CVE-2026-116692 is remote code execution on MLflow's own machine learning tracking server: any HTTP caller who can reach the server can upload a Python module and have the server import and run it, without credentials on a default deployment. The GitHub advisory rates it Critical, CVSS v3.1 9.8 (
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H). - MLflow 3.12.0 through 3.16.x are affected, as is the 3.12.0rc0 release candidate. The first patched release is 3.17.0, released October 7, 2026.
- The bug is a chain: the server's third-party scorer deserialization accepts any module path that starts with an allow-listed package prefix, and the server's own experiment and artifact endpoints let a caller point an experiment's artifact location at such a package and upload a Python file beneath it. Scorers are MLflow's evaluation functions for LLM application and agent output, and third-party scorers wrap metrics from libraries such as Ragas, DeepEval, TruLens and Phoenix. On the official MLflow v3.14.0 image the process runs as root, so the code executes as root.
- No exploitation in the wild is known and the CVE is not in CISA's KEV catalog. A harmless proof of concept is attached to the advisory.
- Update to 3.17.0 and recreate any server containers. If you cannot update today, restrict who can reach the server: basic authentication does not stop an authenticated user who can create experiments and upload artifacts.
Am I affected?
You are affected if you run an MLflow tracking server from 3.12.0 up to, but not including, 3.17.0, if its HTTP endpoints are reachable by callers who should not be able to create experiments and upload artifacts, and if its process can write inside one of its installed scorer packages. All three of these must hold:
- You run an MLflow tracking server at version 3.12.0 (including its release candidates) or later, below 3.17.0. The third-party scorer feature that carries the bug first appears in 3.12.0.
- The server's HTTP endpoints (experiment, run, artifact upload and the scorer invocation route) are reachable by callers who should not be able to create experiments and upload artifacts. Authentication is off by default, and
mlflow serverbinds to127.0.0.1unless started with another--host, such as0.0.0.0. A server that listens beyond loopback, or sits behind a proxy that forwards to it, exposes those endpoints to anyone who can reach its port. With basic authentication enabled, the exposure narrows to any authenticated principal who can create experiments and runs and upload artifacts. - The server process can write beneath one of its installed scorer packages. The advisory notes this holds in the official MLflow image and in virtual environments installed and run by the same operating-system user; a read-only container filesystem or a scorer package directory the server process cannot write to blocks the exact placement chain.
| Package | Affected versions | Fixed version |
|---|---|---|
| mlflow (PyPI) | 3.12.0rc0 to 3.16.x | 3.17.0 (October 7, 2026) |
For scale, PyPI recorded 4,880,597 mlflow downloads in the week before October 28, 2026; that counts every download, not only tracking servers.
Check the running server first, then compare it with the package installed in that deployment. Package metadata alone does not prove that a long-running process has restarted after an upgrade:
# Running server process (include your deployment authentication if required)
curl -fsS http://<server>:<port>/version
# Installed package: run inside the deployment with the server interpreter/user
docker exec <container> python -c "import mlflow; print(mlflow.__version__)"
python -c "import mlflow; print(mlflow.__version__)"
# On-disk package metadata only; this does not inspect the running process
python -m pip show mlflow | grep Version
# Dependency files (the advisory lists only the mlflow package, not mlflow-skinny)
grep -iE "^mlflow([^a-z0-9._-]|$)" requirements*.txt
grep -A10 '"mlflow"' Pipfile.lock | grep '"version"'
How do I fix it?
Upgrade the server package to 3.17.0 and recreate server containers, so the running process is replaced and not just the image on disk:
pip install -U "mlflow>=3.17.0"
pip install -U only replaces the files on disk: a server process started before the upgrade keeps the vulnerable code in memory until it restarts. Restart the server process (for example with systemctl restart on its unit, or your process supervisor's equivalent), then ask the running server for its version through its /version endpoint. It must return 3.17.0 or later:
curl -s http://<server>:<port>/version
For a containerized server, change the image tag, pull and recreate the container; a restarted old container keeps the old code. Then run the docker exec version check above against the new container: it must print 3.17.0 or later.
The same release also fixes two companion issues:
- CVE-2026-116690: arbitrary file read on unauthenticated servers through the source path of a model version tagged as a prompt (CVSS v3.1 7.5, affects 3.10.0 through 3.16.x); the FAQ below explains the mechanism.
- CVE-2026-116691: CrewAI autologging records LLM provider API keys in logged trace spans (CVSS v3.1 6.5, affects 2.19.0 through 3.16.x).
If you cannot upgrade today, three mitigations reduce the window, each with limits:
- Restrict network reachability of the tracking server (bind to loopback, VPN, an authenticating ingress or an IP allowlist). Limit: this shrinks the attack surface but patches nothing; any caller still inside the boundary can exploit the endpoint.
- Enable MLflow's basic authentication. Limit: the scorer invocation endpoint is not gated by a resource-specific permission check, so an authenticated principal with experiment, run and artifact access remains a threat.
- Run the server with a read-only container filesystem or with scorer package directories the server process cannot write to. Limit: this blocks this exact file-placement chain; it does not fix the underlying deserialization behavior.
What is the root cause?
The scorer deserializer treats a string prefix as a provenance boundary and then imports whatever module the request names, while the server's own artifact endpoints let a caller place a module inside that boundary. MLflow 3.12.0 introduced registration and deserialization support for third-party scorers in open-source MLflow (commit 02eea260, pull request #22634). At the v3.14.0 release commit, mlflow/genai/scorers/scorer_utils.py defines the allowed roots:
THIRD_PARTY_SCORER_ALLOWED_MODULES = frozenset({
"mlflow.genai.scorers.ragas",
"mlflow.genai.scorers.deepeval",
"mlflow.genai.scorers.trulens",
"mlflow.genai.scorers.phoenix",
})
In mlflow/genai/scorers/base.py, the deserializer accepts either an exact root or any descendant, then performs three operations with request-controlled values:
module_path == m or module_path.startswith(m + ".")
...
module = importlib.import_module(module_path)
scorer_class = getattr(module, class_name, None)
scorer_instance = scorer_class(**init_kwargs)
A package-name prefix is not a provenance boundary. If the server process can write below an allowed package, an attacker-created descendant satisfies the string check and becomes importable code.
The other half of the chain is the artifact layout. The experiment creation endpoint accepts an artifact_location, and MLflow derives each run's artifact directory as <artifact_location>/<run_id>/artifacts/. In the official MLflow v3.14.0 image, a caller can set an experiment's artifact location to file:///usr/local/lib/python3.10/site-packages/mlflow/genai/scorers/trulens. The caller then uploads a module through the run-artifact endpoint, and the file lands at the import path mlflow.genai.scorers.trulens.<run_id>.artifacts.<module>, inside the allow-listed prefix.
Finally, in mlflow/server/handlers.py, _invoke_scorer_handler() deserializes and constructs the scorer before it validates or resolves the supplied trace IDs, so the attacker-controlled constructor runs first. With authentication disabled, as in the default reproduced configuration, no credentials are required.
How is it exploited?
A caller who can reach the server uses MLflow's own experiment, run and artifact endpoints to write a Python module inside an allowed scorer package, then has the scorer invocation route import that module and construct a class from it. The proof of concept attached to the advisory chains these endpoints, end to end:
- Create an experiment whose
artifact_locationpoints at a writable allowed scorer package, for examplefile:///usr/local/lib/python3.10/site-packages/mlflow/genai/scorers/trulensin the official image. - Create a run, which makes MLflow allocate
<artifact_location>/<run_id>/artifacts/. - Upload a Python module,
audit_<token>.py, into that run artifact directory through the run-artifact upload endpoint. - Submit a serialized third-party scorer whose
modulepath resolves to the uploaded file:mlflow.genai.scorers.trulens.<run_id>.artifacts.audit_<token>. - Trigger the import and class construction through
POST /ajax-api/3.0/mlflow/scorer/invoke.
The attacker-controlled constructor then executes in the MLflow server process. In the proof of concept (PoC), the constructor writes a random marker file beside its own uploaded artifact, and the PoC then downloads that file back through the run-artifact endpoint and checks it for an exact match, proving execution. The later trace-processing failure and HTTP 500 occur after the constructor has already run, so the 500 does not disprove exploitation.
The advisory's remote reproduction ran the same chain against a disposable cloud host running the official v3.14.0 image, pinned by manifest digest, with the process running as uid 0 and the marker file reporting whoami: root.
artifact_location inside an
allowed scorer package"] --> B["Create run:
<pkg>/<run_id>/artifacts/"] B --> C["Upload audit_<token>.py
as a run artifact"] C --> D["Serialized scorer names
mlflow.genai.scorers.trulens.
<run_id>.artifacts.audit_<token>"] D --> E["POST /scorer/invoke:
prefix check passes,
module not yet validated"] E --> F["import_module + constructor
execute in the server process"] F --> G["Code runs with server
privileges (uid 0 in the
official image)"]
What can an attacker do?
On the reproduced stock no-auth deployment, any network caller who reaches the tracking server executes Python code as the MLflow process, with no credentials and no user interaction. The advisory spells out the blast radius: full confidentiality, integrity and availability impact on all data, credentials, network access and filesystem locations available to the server process. The official image runs the process as root, which raises container-level impact; host impact still depends on the container runtime configuration, mounted paths, granted capabilities and socket exposure.
No weaponized exploit is public as of October 28, 2026.
Why does the fix work?
The fix, commit a732443e ("Match third-party scorer modules exactly instead of by package prefix", pull request #25906, merged September 16, 2026), replaces the prefix match with exact matching:
- if not any(
- module_path == m or module_path.startswith(m + ".")
- for m in THIRD_PARTY_SCORER_ALLOWED_MODULES
- ):
+ if module_path not in THIRD_PARTY_SCORER_ALLOWED_MODULES:
The allow-list itself grew to name every concrete scorer module verbatim, for example mlflow.genai.scorers.ragas.scorers.rag_metrics, so the legitimate imports keep working. A caller-placed descendant such as mlflow.genai.scorers.trulens.<run_id>.artifacts.audit_<token> no longer matches anything on the list, so the artifact-placement chain is broken. Three further gates block the remaining import paths a request could reach:
metric_namemust be a plain Python identifier, because MLflow's Ragas and DeepEval scorer wrappers resolve unknown metric names by splicing them into an import path such asragas.metrics.collections.<metric_name>ordeepeval.metrics.<metric_name>.kwargsmay not override the validatedmetric_name.- After the import, the fetched class must be a
Scorersubclass before it is instantiated.
Together these make the deserializer import only the modules MLflow itself ships, which is what the original design intended.
How do I check if I was compromised?
The published material describes the proof of concept, not attacker indicators, so no public indicator list exists. From the advisory's own mechanics, an affected deployment can check for the traces the chain leaves:
- Look for
.pyfiles inside the installed scorer packages that are newer than MLflow's own__init__.py. Run the commands with the server's interpreter, so that$MLFLOW_ROOTis the path the server imports from:
# Files placed inside installed scorer packages (run with the server's interpreter)
MLFLOW_ROOT=$(python -c "import mlflow, os; print(os.path.dirname(mlflow.__file__))")
find "$MLFLOW_ROOT/genai/scorers/" -type f -name '*.py' -newer "$MLFLOW_ROOT/__init__.py"
- Audit the tracking store for experiments whose
artifact_locationis afile://path inside the Python installation, in particular undersite-packages/mlflow/genai/scorers/; the proof of concept uses exactly this pattern. On a SQL backend store, query theexperimentstable (the SQLite example below works the same withpsqlormysql); on a file-basedmlrunsstore, each experiment'smeta.yamlholds itsartifact_location:
# Experiments whose artifact location points into the scorer packages
sqlite3 mlflow.db "SELECT experiment_id, name, artifact_location FROM experiments WHERE artifact_location LIKE '%/mlflow/genai/scorers%';"
grep -l 'mlflow/genai/scorers' mlruns/*/meta.yaml
- Search server access logs for
POSTrequests to/ajax-api/3.0/mlflow/scorer/invokewhose payloads carrythird_party_scorer_datawith amodulepath that contains an.artifacts.segment. The chain always produces a path shapedmlflow.genai.scorers.<package>.<run_id>.artifacts.<module>, with the 32-character hexadecimal run ID just before.artifacts., and no module MLflow ships has that segment. - Search server access logs for uploads of
.pyfiles. MLflow's run-artifact upload route,POST /ajax-api/2.0/mlflow/upload-artifact, takes the destination file name in itspathquery parameter, so this check still works after the uploaded file itself has been deleted:
grep -E 'POST /ajax-api/2\.0/mlflow/upload-artifact\?[^ ]*path=[^ &]*\.py' access.log
A clean log review is weak assurance, in particular if the server ran with authentication disabled on an open network for any period on an affected version. Every step goes through MLflow's own experiment, run, artifact and scorer endpoints, so no request targets an unusual endpoint, and only details such as the ones above set the requests apart from legitimate use. The traces that persist are the experiment record with its file:// artifact_location and the run directory holding the uploaded module. In the proof of concept, a marker file also sits beside the module. Code running as the server process can delete all of these traces.
If you find any of these traces, treat the server as compromised rather than only upgrading it in place. The advisory puts all data, credentials, network access and filesystem locations available to the server process in scope, so isolate the server, rotate every credential that process could read, and recreate it from a clean 3.17.0 or later image.
Timeline
- 2026-07-12: the advisory's sanitized remote reproduction output records this date as the server time, and the advisory states that no fixed release was identified as of that date (GitHub advisory, proof-of-concept transcript).
- 2026-09-16: fix commit
a732443eis merged intomasterthrough pull request #25906: exact module matching plus the identifier, kwargs and subclass gates (GitHub commit). - 2026-10-07: MLflow 3.17.0 released with the fix (GitHub releases).
- 2026-10-09: MLflow publishes repository advisory GHSA-26p8-2jq9-3vq9 (08:20 UTC), rating the issue Critical, CVSS v3.1 9.8 (GitHub security advisory).
- 2026-10-27: the GitHub Advisory Database lists GHSA-26p8-2jq9-3vq9 (21:26 UTC).
- 2026-10-28: this post.
The fix itself came more than two months after the July reproduction. For an affected server exposed since July and not upgraded on October 7, the only public signals before the October 9 advisory were the September 16 fix commit and a one-line 3.17.0 release-note entry, both carrying the title "Match third-party scorer modules exactly instead of by package prefix (GHSA-26p8-2jq9-3vq9)". Dependency scanners had nothing to report for longer still: Dependabot alerts draw on the GitHub Advisory Database, which listed the advisory on October 27, and the OSV entry carries the same date, so a patch process driven by those alerts got no signal for 18 days after the advisory and 20 days after the fixed release.
FAQ
Does my MLflow client put me at risk, or only a self-hosted server?
Only the server side. The chain needs the tracking server's own HTTP endpoints: experiment creation with a chosen artifact_location, run creation, artifact upload and scorer invocation. If you only use MLflow as a client against a server someone else operates and patches, your client version does not create this exposure.
Does enabling authentication stop it?
It stops the unauthenticated variant and leaves the authenticated one. The advisory finds that the scorer invocation endpoint is assigned to validate_gateway_proxy, which returns true without an experiment- or scorer-level resource permission check, so any authenticated principal who can create experiments, create runs and upload artifacts can still reach the constructor.
Why is the CVSS score 9.8 when a read-only filesystem blocks it?
The 9.8 vector (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H) scores the default deployment the advisory reproduced: network-reachable, no authentication, no user interaction, and full impact on confidentiality, integrity and availability. The read-only-filesystem condition is a hardening measure that happens to break one link of this specific chain, not a property of the software's default configuration.
Are the companion issues fixed in 3.17.0 worth patching on their own?
CVE-2026-116690 is an arbitrary file read on unauthenticated servers (CVSS v3.1 7.5, affects 3.10.0 and later, before 3.17.0): a model version tagged as a prompt can take a schemeless relative path such as ../../../tmp as its source, and the server then serves files from that path. It reaches deployments two minor versions older than the RCE.
CVE-2026-116691 (CVSS v3.1 6.5, affects 2.19.0 and later, before 3.17.0) records LLM provider API keys in logged trace spans when CrewAI autologging is on, which is a data-exposure issue in any deployment that shares traces.
One upgrade to 3.17.0 covers all three.
Is there a public exploit?
A proof of concept is attached to the advisory and is deliberately harmless: it writes a marker file, downloads it back and checks the match. It demonstrates execution without publishing a weapon. No exploitation in the wild is known and the CVE is not in the KEV catalog, as of October 28, 2026.
Find exposed MLflow servers with Ostorlab
Run an Ostorlab web and API scan across your environments: it identifies exposed MLflow tracking-server endpoints and the versions they run, so you can confirm every deployment is on 3.17.0 or later and that no unauthenticated instance is reachable from the internet.
References
- GitHub advisory GHSA-26p8-2jq9-3vq9, CVE-2026-116692, CVSS v3.1 9.8, published October 9, 2026 and listed in the GitHub Advisory Database on October 27, 2026, with the full technical analysis and proof-of-concept attachments: https://github.com/mlflow/mlflow/security/advisories/GHSA-26p8-2jq9-3vq9 (checked October 28, 2026)
- Fix commit
a732443e, "Match third-party scorer modules exactly instead of by package prefix", September 16, 2026: https://github.com/mlflow/mlflow/commit/a732443e695c3c677680c54320efc394c81654ab (read in full October 28, 2026) - Introducing change
02eea260, "Enable third-party scorer registration in OSS MLflow (#22634)", PR #22634: https://github.com/mlflow/mlflow/pull/22634 (checked October 28, 2026) - MLflow 3.17.0 release, October 7, 2026: https://github.com/mlflow/mlflow/releases/tag/v3.17.0
- Companion advisories: GHSA-m65p-9j7q-6w33 (CVE-2026-116690) and GHSA-j7qq-vr8c-xvw3 (CVE-2026-116691): https://github.com/advisories/GHSA-m65p-9j7q-6w33, https://github.com/advisories/GHSA-j7qq-vr8c-xvw3 (checked October 28, 2026)
- Download reach, 4,880,597 weekly PyPI downloads: https://pypistats.org/packages/mlflow (checked October 28, 2026)
- CISA Known Exploited Vulnerabilities catalog, confirming CVE-2026-116692 is not listed: https://www.cisa.gov/known-exploited-vulnerabilities-catalog (checked October 28, 2026)