Root cause
Authentication & access control introduced
The change added the assistant API router and mounted it on the tracking server under the /ajax-api/ prefix, with only a loopback source-address check standing in for access control.
How AI contributed: Direct introductionPublished May 19, 2026 in
mlflow/mlflowAll 7 findings in this repository
The assistant service first enters the tree with its own API router, guarded only by a check that the request comes from a loopback address and mounted under the /ajax-api/3.0/mlflow/assistant prefix, while the server's request-blocking path test and its wildcard cross-origin response stayed as they were. The repair teaches that path test about the /ajax-api/ prefix, so state-changing requests to the assistant are inspected and refused unless the request origin is loopback or explicitly configured, and it replaces the wildcard response with the configured origins plus the loopback origin patterns.
A user who runs the tracking server locally with the assistant enabled and then opens a page from anywhere on the web gives that page's script the same source address as the local user, so the loopback check that guards the assistant routes passes. Those routes are mounted under /ajax-api/3.0/mlflow/assistant, a prefix the server's cross-origin request blocking never inspected because its path test only matched /api/ endpoints, and the fallback cross-origin middleware answered the page's preflight with a wildcard allow-origin. The page can therefore send a state-changing request to the assistant configuration endpoint and rewrite which providers, projects and permissions the assistant runs with; turning on full access makes the assistant launch its coding agent with permission prompts bypassed, which is arbitrary command execution in the account that started the server.
Root cause
The change added the assistant API router and mounted it on the tracking server under the /ajax-api/ prefix, with only a loopback source-address check standing in for access control.
Fix
The repair extends the API path test to the /ajax-api/ prefix so cross-origin blocking covers the assistant routes, and stops the fallback cross-origin middleware from echoing any origin.
Fix by TomuHirata · no AI marker found
Case facts
@@ -11,6 +11,7 @@ from fastapi.middleware.wsgi import WSGIMiddleware from flask import Flask from mlflow.server import app as flask_app+from mlflow.server.assistant.api import assistant_router from mlflow.server.fastapi_security import init_fastapi_security from mlflow.server.gateway_api import gateway_router from mlflow.server.job_api import job_api_router@@ -50,6 +51,10 @@ def create_fastapi_app(flask_app: Flask = flask_app): # This provides /gateway/{endpoint_name}/mlflow/invocations routes fastapi_app.include_router(gateway_router) + # Include Assistant API router for AI-powered trace analysis+ # This provides /ajax-api/3.0/mlflow/assistant/* endpoints (localhost only)+ fastapi_app.include_router(assistant_router)+ # Mount the entire Flask application at the root path # This ensures compatibility with existing APIs # NOTE: This must come AFTER include_router to avoid Flask catching all requests@@ -23,8 +23,9 @@ STATE_CHANGING_METHODS = ["POST", "PUT", "DELETE", "PATCH"] # Paths exempt from host validation HEALTH_ENDPOINTS = ["/health", "/version"] -# API path prefix for MLflow endpoints+# API path prefixes for MLflow endpoints API_PATH_PREFIX = "/api/"+AJAX_API_PATH_PREFIX = "/ajax-api/" # Test-only endpoints that should not have CORS blocking TEST_ENDPOINTS = ["/test", "/api/test"]@@ -122,7 +123,9 @@ def should_block_cors_request(origin: str, method: str, allowed_origins: list[st def is_api_endpoint(path: str) -> bool: """Check if a path is an API endpoint that should have CORS/OPTIONS handling."""- return path.startswith(API_PATH_PREFIX) and path not in TEST_ENDPOINTS+ return (+ path.startswith(API_PATH_PREFIX) or path.startswith(AJAX_API_PATH_PREFIX)+ ) and path not in TEST_ENDPOINTS def is_allowed_host_header(allowed_hosts: list[str], host: str) -> bool:@@ -175,10 +176,13 @@ def init_fastapi_security(app: FastAPI) -> None: expose_headers=["*"], ) else:+ # Use CORSBlockingMiddleware for blocking CORS requests on the server side,+ # and CORSMiddleware for responding to OPTIONS requests. app.add_middleware(CORSBlockingMiddleware, allowed_origins=allowed_origins) app.add_middleware( CORSMiddleware,- allow_origins=["*"],+ allow_origins=allowed_origins,+ allow_origin_regex="|".join(LOCALHOST_ORIGIN_PATTERNS), allow_credentials=True, allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"], allow_headers=["*"],