Oct 28, 2026·9 min read·5 visits
An insecure module prefix check in MLflow's third-party GenAI scorer deserialization allows attackers to upload malicious Python artifacts into writable package directories and execute arbitrary code via dynamic module importation on unauthenticated or authenticated MLflow servers.
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.
MLflow is an open-source platform designed to manage the machine learning and generative AI lifecycle, including experiment tracking, model registry, and evaluation. Starting in version 3.12.0rc0, MLflow introduced support for registering and invoking third-party GenAI evaluation scorers from external libraries (ragas, deepeval, trulens, and phoenix). To support remote evaluation workflows, the MLflow tracking server exposes the /ajax-api/3.0/mlflow/scorer/invoke HTTP endpoint, which accepts serialized scorer definitions and reconstructs the corresponding Python objects on the server.
In versions 3.12.0rc0 through 3.16.1, the deserialization routine in mlflow/genai/scorers/base.py validates incoming module paths using a string prefix comparison against an allowlist of four base package namespaces (THIRD_PARTY_SCORER_ALLOWED_MODULES). Because the validation checks whether the requested module path begins with an allowed package prefix followed by a dot (module_path.startswith(m + ".")), any descendant submodule path beneath those package trees passes validation.
This validation flaw interacts directly with MLflow's experiment creation and artifact storage architecture. When creating an experiment, a caller can specify an arbitrary local file:// URI as the artifact_location. If the server process has write permissions to its own Python site-packages directory—which is the default state in official MLflow Docker images running as root—an attacker can point an experiment's artifact root to one of the allowlisted scorer package directories on the local filesystem.
After uploading a Python file as a run artifact under that experiment, the attacker submits a request to /ajax-api/3.0/mlflow/scorer/invoke referencing the dotted module path of the uploaded file. The server's Scorer.model_validate_json() method imports the attacker-placed module via importlib.import_module() and instantiates the specified class before performing any trace evaluation. This results in arbitrary code execution within the security context of the MLflow server process, classified under CWE-94 (Code Injection) and CWE-470 (Unsafe Reflection).
The root cause of CVE-2026-116692 is the unsafe convergence of three architectural behaviors in MLflow: package-prefix validation during dynamic module importation, unrestricted local filesystem paths for experiment artifact storage, and premature object deserialization prior to trace processing.
First, the deserialization method Scorer.model_validate() in mlflow/genai/scorers/base.py relied on THIRD_PARTY_SCORER_ALLOWED_MODULES defined in mlflow/genai/scorers/scorer_utils.py. Instead of restricting imports to a closed set of exact module names, the validation logic checked module_path == m or module_path.startswith(m + "."). In Python, a dotted module path corresponds directly to the underlying directory hierarchy on the filesystem. Consequently, treating a package prefix as a trust boundary fails if an attacker can write files into any subdirectory beneath that package's filesystem root.
Second, MLflow's experiment creation endpoint allows callers to define the artifact_location parameter using local file:// URIs without restricting the target path to a dedicated storage sandbox. When a run is created within a file-backed experiment, MLflow constructs the artifact storage path using the template <artifact_location>/<run_id>/artifacts/. By setting artifact_location to file:///usr/local/lib/python3.10/site-packages/mlflow/genai/scorers/trulens, any file uploaded via the artifact upload API is written directly into the trulens scorer package directory tree on disk.
Third, in mlflow/server/handlers.py, the _invoke_scorer_handler() function deserializes the user-supplied serialized_scorer JSON payload immediately upon receiving the request:
scorer = Scorer.model_validate_json(serialized_scorer)
batches = get_trace_batches_for_scorer(trace_ids, scorer, tracking_store)During Scorer.model_validate_json(), the server calls importlib.import_module(module_path), looks up the caller-specified class_name via getattr(), and invokes scorer_class(**init_kwargs) without first verifying that the target class inherits from Scorer. Even if the resulting object later fails trace evaluation and causes the endpoint to return an HTTP 500 error, both top-level module code and the class constructor (__init__) have already executed synchronously.
The vulnerability was introduced in commit 02eea26035b9d8e65af31e6e8f058ca195f62cdd (Pull Request #22634) and remediated in commit a732443e695c3c677680c54320efc394c81654ab (Pull Request #25906). The patch modifies mlflow/genai/scorers/base.py and mlflow/genai/scorers/scorer_utils.py to enforce exact module matching, validate metric identifiers, block keyword argument overrides, and verify class inheritance before instantiation.
In mlflow/genai/scorers/scorer_utils.py, the allowlist was expanded from four top-level package names to an explicit enumeration of all valid submodules that define supported scorer classes:
# mlflow/genai/scorers/scorer_utils.py (Patched)
THIRD_PARTY_SCORER_ALLOWED_MODULES = frozenset({
"mlflow.genai.scorers.ragas",
"mlflow.genai.scorers.ragas.scorers",
"mlflow.genai.scorers.ragas.scorers.agentic_metrics",
"mlflow.genai.scorers.ragas.scorers.comparison_metrics",
"mlflow.genai.scorers.ragas.scorers.rag_metrics",
"mlflow.genai.scorers.deepeval",
"mlflow.genai.scorers.deepeval.scorers",
"mlflow.genai.scorers.deepeval.scorers.agentic_metrics",
"mlflow.genai.scorers.deepeval.scorers.conversational_metrics",
"mlflow.genai.scorers.deepeval.scorers.rag_metrics",
"mlflow.genai.scorers.deepeval.scorers.safety_metrics",
"mlflow.genai.scorers.trulens",
"mlflow.genai.scorers.trulens.scorers.agent_trace",
"mlflow.genai.scorers.phoenix",
})In mlflow/genai/scorers/base.py, the deserialization routine in Scorer.model_validate() was hardened across four distinct vectors:
# mlflow/genai/scorers/base.py (Vulnerable vs. Patched)
module_path = data.get("module") or ""
class_name = data.get("class")
metric_name = data.get("metric_name")
-
- # VULNERABLE: Prefix check allows arbitrary descendant paths
- if not any(
- module_path == m or module_path.startswith(m + ".")
- for m in THIRD_PARTY_SCORER_ALLOWED_MODULES
- ):
+
+ # FIX 1: Exact membership check prevents importing descendant artifact paths
+ if module_path not in THIRD_PARTY_SCORER_ALLOWED_MODULES:
raise MlflowException.invalid_parameter_value(
f"Third-party scorer '{serialized.name}': module '{module_path}' is not "
f"in the allow-list {sorted(THIRD_PARTY_SCORER_ALLOWED_MODULES)}."
)
if not class_name or not metric_name:
raise MlflowException.invalid_parameter_value(
f"Third-party scorer '{serialized.name}': missing required fields in "
f"third_party_scorer_data (class, metric_name)."
)
+
+ # FIX 2: Prevent dotted paths in metric_name from triggering secondary imports
+ if not metric_name.isidentifier():
+ raise MlflowException.invalid_parameter_value(
+ f"Third-party scorer '{serialized.name}': metric_name '{metric_name}' "
+ "must be a plain identifier."
+ )
+
+ # FIX 3: Prevent kwargs['metric_name'] from overriding the validated metric_name
+ if "metric_name" in (data.get("kwargs") or {}):
+ raise MlflowException.invalid_parameter_value(
+ f"Third-party scorer '{serialized.name}': kwargs must not contain "
+ "'metric_name'; set it at the top level of third_party_scorer_data."
+ )
try:
module = importlib.import_module(module_path)
except ImportError as e:
@@ -602,6 +617,12 @@
f"Third-party scorer '{serialized.name}': class '{class_name}' not "
f"found in module '{module_path}'."
)
+
+ # FIX 4: Ensure the target attribute is a valid Scorer subclass before calling __init__
+ if not (inspect.isclass(scorer_class) and issubclass(scorer_class, Scorer)):
+ raise MlflowException.invalid_parameter_value(
+ f"Third-party scorer '{serialized.name}': '{module_path}.{class_name}' "
+ "is not a Scorer subclass."
+ )
init_kwargs: dict[str, Any] = dict(data.get("kwargs") or {})> [!NOTE]
> Fix Completeness Assessment: The patch goes beyond merely replacing the startswith() check. The authors identified a secondary bypass vector where third-party wrapper classes (such as RagasScorer and DeepEvalScorer) dynamically resolve unknown metric names by splicing metric_name into secondary importlib calls (e.g., ragas.metrics.collections.<metric_name> or deepeval.metrics.<metric_name>). By enforcing metric_name.isidentifier() and prohibiting metric_name inside kwargs, the patch prevents attackers from pivoting to an unvalidated dotted import path inside the third-party library wrappers.
Exploiting CVE-2026-116692 requires network access to the MLflow tracking server and write permissions for the MLflow server process on one of the allowed scorer package directories (or the third-party evaluation library directories). In default MLflow deployments, authentication is disabled, allowing unauthenticated remote attackers to execute the full chain. When basic authentication is enabled, the attacker must authenticate as a user permitted to create experiments, start runs, and upload artifacts.
The exploitation sequence consists of four distinct stages:
artifact_location as file:///usr/local/lib/python3.10/site-packages/mlflow/genai/scorers/trulens (adjusting the Python version path to match the target environment). The server registers the experiment and returns an experiment_id.experiment_id. The server returns a 32-character hexadecimal run_id (for example, 9a2d665eb38940b68983f0586f623115).payload.py) to the run. Because the experiment uses a file:// artifact root, MLflow writes the file directly to /usr/local/lib/python3.10/site-packages/mlflow/genai/scorers/trulens/9a2d665eb38940b68983f0586f623115/artifacts/payload.py./ajax-api/3.0/mlflow/scorer/invoke with a serialized scorer JSON payload referencing the uploaded file's dotted module path:{
"name": "eval_exploit",
"third_party_scorer_data": {
"module": "mlflow.genai.scorers.trulens.9a2d665eb38940b68983f0586f623115.artifacts.payload",
"class": "ExploitClass",
"metric_name": "ExploitClass",
"model": null,
"kwargs": {}
}
}In authoritative testing against the official ghcr.io/mlflow/mlflow:v3.14.0 container image, invoking /ajax-api/3.0/mlflow/scorer/invoke with the payload mlflow.genai.scorers.trulens.9a2d665eb38940b68983f0586f623115.artifacts.audit_1783843760683972103 caused the server to import the artifact module as uid 0 (root). Although the HTTP response returned status code 500 during subsequent trace batch retrieval, the payload had already executed and written a verification marker file into the artifact directory, which was subsequently confirmed via a 200 OK artifact download request.
Successful exploitation of CVE-2026-116692 grants the attacker arbitrary code execution within the context of the MLflow tracking server process. The vulnerability is rated 9.8 (Critical) under CVSS v3.1 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H), reflecting network-accessible exploitation with low complexity, no user interaction, and no required privileges in default configurations.
In official container deployments (such as ghcr.io/mlflow/mlflow:v3.14.0), the MLflow server runs as the root user (uid 0), and the system Python site-packages directory (/usr/local/lib/python3.10/site-packages) is writable by the running process. Consequently, an attacker compromises the entire container environment with administrative privileges.
From this position, an attacker can access sensitive machine learning assets managed by the server, including proprietary model weights, training datasets, evaluation traces, and database credentials used by the backend tracking store. Furthermore, MLflow tracking servers frequently hold cloud provider credentials (such as AWS IAM roles, GCS service account keys, or Azure storage tokens) and third-party LLM API keys (e.g., OpenAI or Anthropic credentials) configured for AI gateway and evaluation features.
Even in environments where MLflow's built-in basic authentication is enabled, the /ajax-api/3.0/mlflow/scorer/invoke endpoint is only guarded by validate_gateway_proxy, which returns True without verifying experiment-level or scorer-level resource permissions. Any authenticated user with basic permissions to create an experiment and upload a run artifact can escalate their access to full server-side code execution.
To remediate CVE-2026-116692, organizations must upgrade MLflow to version 3.17.0 or later, which incorporates commit a732443e695c3c677680c54320efc394c81654ab. The update can be applied via pip:
pip install --upgrade "mlflow>=3.17.0"For environments where immediate upgrading is not feasible, security teams can break the exploitation chain by enforcing strict filesystem permissions. Because the attack requires the MLflow server process to write artifacts directly into Python package directories on disk, configuring the Python installation directory (site-packages and all directories in sys.path) as read-only to the user running the MLflow server prevents the artifact upload stage from succeeding.
In containerized deployments, operators should avoid running the MLflow server as root. Containers should be executed under an unprivileged UID with a read-only root filesystem (--read-only), mounting writable volumes exclusively for dedicated, isolated artifact and database staging directories outside the Python module search path.
Additionally, administrators should configure MLflow with --serve-artifacts backed by remote object storage (such as Amazon S3, Google Cloud Storage, or Azure Blob Storage) rather than allowing local file:// artifact locations. Finally, MLflow tracking servers should never be exposed to untrusted networks without strong authentication and access controls enforced via a reverse proxy.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
mlflow mlflow | >= 3.12.0rc0, < 3.17.0 | 3.17.0 |
| Attribute | Detail |
|---|---|
| CVE / GHSA ID | CVE-2026-116692 / GHSA-26p8-2jq9-3vq9 |
| CWE Classification | CWE-94 (Code Injection) / CWE-470 (Unsafe Reflection) |
| CVSS v3.1 Score | 9.8 Critical (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H) |
| Affected Versions | MLflow >= 3.12.0rc0, < 3.17.0 |
| Patched Version | MLflow 3.17.0 |
| Vulnerable Component | Scorer.model_validate() in mlflow/genai/scorers/base.py |
| Exploit Status | Proof of Concept (PoC) Published |
| CISA KEV Status | Not Listed |
The software constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment.
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-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.
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.
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.