Loopback Is Not a Trust Boundary: CVE-2026-2611
On May 19, 2026, the National Vulnerability Database published CVE-2026-2611. MLflow version 3.9.0’s Assistant feature had improper origin validation on its local /ajax-api endpoints. A malicious webpage loaded in any tab could issue cross-origin requests to the Assistant’s local server, modify its configuration to enable full-access mode, then execute arbitrary commands through the bundled Claude Code CLI integration. Huntr scored the vulnerability CVSS 3.0 at 9.6 (CRITICAL) with vector AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H: network-reachable, low complexity, no privileges required, scope-changed, full confidentiality / integrity / availability impact.1
The root cause is not novel and the fix is not exotic, but the threat model is new. Any local service that binds to loopback and exposes mutation endpoints to a browser tab is a trust-boundary surface. When the service drives an AI agent that can read files, run shell commands, and push code, “loopback only” stops working as a security control. CVE-2026-2611 is the textbook case for an entire class of vulnerabilities that will land repeatedly as more developer tools wrap LLM agents inside local web UIs.
TL;DR
CVE-2026-2611 documents an incomplete CORS-enumeration bug in MLflow 3.9.0. The Assistant feature exposed /ajax-api/ endpoints that MLflow’s is_api_endpoint() check did not cover, so the CORS-blocking middleware skipped them. The legacy CORSMiddleware allowed allow_origins=["*"] on top of that, so a cross-origin request from evil.com could call /ajax-api/3.0/mlflow/assistant/config, flip the Assistant to full-access mode, then send shell commands through the bundled Claude Code CLI integration.1 The upstream fix (PR #20832, “Block CORS for ajax paths”) in MLflow 3.10.0 adds /ajax-api/ to the protected-path enumeration and tightens allow_origins to the configured allowlist plus a localhost regex.2 Defenses below cover the local-AI-agent class, not just MLflow.
Key Takeaways
| Role | Action |
|---|---|
| AI/ML engineers running MLflow | Upgrade to MLflow 3.10.0; audit any other local AI-agent UI that listens on loopback |
| Security teams | Add “local LLM-agent loopback service” to your threat-model templates; one CORS gap equals RCE in this class |
| Tool authors building local agent UIs | Enumerate every API path prefix once; ban allow_origins=["*"] even on localhost-bound servers; gate destructive endpoints on a fresh user gesture, not just origin |
| Engineering leads | Sandbox the team’s AI coding agents and treat their command capability as the actual blast radius |
What the Attack Chain Looks Like
The five-step chain is short. Each step uses authorized capabilities; the composition fails.
| Step | Action | What the attacker controls |
|---|---|---|
| 1 | User runs mlflow ui locally; Assistant listens on loopback (default 127.0.0.1) |
Nothing yet (local service) |
| 2 | User opens evil.com in any tab while MLflow runs |
The page content |
| 3 | evil.com issues fetch('http://127.0.0.1:5000/ajax-api/3.0/mlflow/assistant/config', { method: 'PUT', credentials: 'include', body: ... }) |
The cross-origin request |
| 4 | MLflow accepts the request because /ajax-api/ was not in the CORS-blocking path enumeration; Assistant config flips to full-access |
The Assistant’s runtime configuration |
| 5 | evil.com posts command payloads through the Assistant’s chat endpoint; MLflow invokes the local Claude Code CLI with permission checks bypassed; commands run as the user |
Arbitrary code execution |
The attacker does not need to install anything on the victim’s machine. The attacker needs the victim to load a single tab containing the payload while mlflow ui is running. The attack works from any origin because the legacy CORSMiddleware had allow_origins=["*"] and the path-level CORS-blocking middleware did not cover the /ajax-api/ prefix.2
The Claude Code integration is the privilege amplifier. MLflow’s ClaudeCodeProvider locates the local claude CLI via shutil.which("claude") and instructs users to install it with npm install -g @anthropic-ai/claude-code when absent. When the Assistant’s permissions.full_access configuration flag is on, the provider extends the CLI invocation with --permission-mode bypassPermissions, the Claude Code mode that suppresses interactive permission prompts and lets tool calls execute without user confirmation.3 The agent’s authorized capabilities (shell, file write, HTTP) become a remote shell with natural-language UX, which is exactly what an attacker wants.
The Root Cause Is an Incomplete Path Enumeration
Reading the fix commit makes the bug obvious. MLflow’s security_utils.py had a constant:
API_PATH_PREFIX = "/api/"
and a check:
def is_api_endpoint(path: str) -> bool:
return path.startswith(API_PATH_PREFIX) and path not in TEST_ENDPOINTS
The CORS-blocking middleware called is_api_endpoint() to decide whether to apply origin validation. Any path that did not start with /api/ flowed through as a non-API path. The Assistant introduced /ajax-api/ for its browser-facing endpoints. Nobody updated is_api_endpoint() to include the new prefix.
The fix is a single-line behavior change in security_utils.py:2
API_PATH_PREFIX = "/api/"
AJAX_API_PATH_PREFIX = "/ajax-api/"
def is_api_endpoint(path: str) -> bool:
return (
path.startswith(API_PATH_PREFIX) or path.startswith(AJAX_API_PATH_PREFIX)
) and path not in TEST_ENDPOINTS
Plus a parallel fix in fastapi_security.py that replaces allow_origins=["*"] on the legacy CORSMiddleware with the configured allowlist plus a localhost regex. The combined change is 5 added lines, 2 deleted in security_utils.py and 5 added, 1 deleted in fastapi_security.py.2
That is the entire vulnerability. CVE-2026-2611 happened because a new endpoint prefix shipped without anyone updating the single function that enumerates which paths get CORS protection. The attack surface was a forgotten config constant.
Why This Pattern Will Repeat
The bug is older than MLflow and older than AI agents. Web frameworks have shipped path-enumeration mistakes for as long as path enumeration has existed. What changed is the consequence. A local web UI that forgets to enumerate one endpoint used to leak some configuration data or trigger a denial of service. A local web UI that forgets to enumerate one endpoint while running an AI coding agent with shell access leaks the entire developer machine.
Local AI agent UIs are a growing category. Any tool that wraps an LLM agent inside a browser UI served from 127.0.0.1 falls in scope: experiment trackers with chat panels (the MLflow shape), IDE plugins that drive an agent through a localhost API, dev-mode servers that an extension communicates with over HTTP, web dashboards for local model runners. Each one binds to loopback. Each one exposes mutation endpoints to a browser tab. Each one runs an agent with permission to shell out, write files, and push to git. Every one of them is exactly one path-enumeration bug away from CVE-2026-2611.
The shared assumption underneath the class is: “the browser’s same-origin policy will protect the loopback server because legitimate requests come from 127.0.0.1 too.” The assumption fails the moment any cross-origin gate (CORS, Host header validation, Origin header validation, fetch metadata headers) misses an endpoint prefix. The browser does not protect the server when the server explicitly accepts the request.
The silent egress attack I covered in March is the same trust assumption broken in the other direction. Silent egress hides the exit through the agent’s authorized tools. Loopback misclassification hides the entry through the browser’s authorized network calls. Both reduce to: an LLM agent’s authorized capabilities are now an attack primitive whenever any single boundary check breaks.
What the Class Actually Looks Like
The vulnerability class has four ingredients. A service is exploitable when all four hold:
- Local web server bound to loopback or
0.0.0.0. Loopback-only is the more common variant. The browser’s CORS, Host, and Origin checks are what protect a localhost-bound service from a cross-origin tab, not the network binding. - Mutation endpoints reachable without a user gesture. Configuration changes, command submissions, file writes. If a
fetch()from an arbitrary origin can mutate state, the service is reachable. - Capability that the local user holds but a webpage should not. Reading the home directory, running shell commands, calling cloud APIs with stored credentials, modifying source files.
- One missing boundary check. Forgotten path prefix, wildcard
allow_origins, missing Host header validation, missing Origin validation, missing Sec-Fetch-Site enforcement.
CVE-2026-2611 hit all four. The fix closes ingredient four for the /ajax-api/ prefix in MLflow specifically. Ingredients one through three still hold in every other local AI agent tool. The next CVE in this class will look identical with a different package name.
Defenses Against the Local AI Agent Loopback Class
The standard CORS advice (set allow_origins to a specific list, enable credentials only when needed) is necessary but not sufficient. The class needs deeper controls.
Enumerate path prefixes in one place, and write a test that fails when a new prefix appears unprotected. MLflow’s bug landed because the team updated is_api_endpoint() when /api/ shipped and forgot it when /ajax-api/ shipped. A test that iterates every registered route on the FastAPI app, asks is_api_endpoint() whether each route is protected, and fails on any unrecognized path prefix would have caught the bug at PR time. The check costs about 20 lines of test code.
Validate Origin and Host headers, not just CORS. CORS protects browsers from the server’s response. It does not protect the server from accepting the request. Local AI agent services should reject requests whose Origin header is not in the explicit allowlist and whose Host header is not 127.0.0.1 or localhost (the latter to prevent DNS rebinding attacks). Both checks should fail closed.
Gate destructive endpoints on Sec-Fetch-Site: same-origin. Browsers send Sec-Fetch-Site: cross-site on any cross-origin fetch(), including from evil.com to 127.0.0.1. The Fetch Metadata Request Headers spec defines the header values, and Chrome, Firefox, Safari, and Edge all send it.4 A middleware that rejects requests to mutation endpoints unless Sec-Fetch-Site: same-origin provides an additional layer that does not depend on remembering every path prefix.
Sandbox the AI agent’s execution capability. Claude Code, Cursor, Aider, and Continue all run with the user’s full privileges by default because that is the easiest UX. A capability sandbox per workspace (bwrap with explicit mount rules on Linux; macOS App Sandbox for tools that ship as .app bundles; container per workspace for cross-platform) reduces blast radius without sacrificing the primary workflow. The mitigation adds friction. The friction is the point.
Treat the agent’s commits as untrusted until reviewed. Branch protection, required reviews, signed commits. The “AI agent with commit access” model breaks down the moment an attacker compromises the agent’s local environment. Reviewer dissent (covered in AI Code Review Needs Dissent) applies here too. If the AI agent and the human reviewer always agree, the human reviewer is decoration.
What the Next CVE in This Class Probably Looks Like
The interesting question is not whether the next CVE-2026-2611-shaped vulnerability lands. The interesting question is which package ships it first. Candidates sit across the local AI tooling ecosystem: experiment trackers that wrap an agent in their UI, IDE plugins that talk to a local agent backend over HTTP, dev-mode servers any tool opens during a session, the localhost APIs extensions use to drive agent UIs. None of these have published a CVE in this shape yet. Several of them ship code paths that bind to loopback and accept cross-origin requests with the browser as the trust gate.
The class is bigger than CVE-2026-2611, and the threat model has shifted. A developer machine in 2026 hosts an AI agent that can shell out, write files, push commits, and call cloud APIs. The local web UIs that drive those agents inherit the agent’s privileges through whatever trust boundary the UI’s HTTP server enforces. CORS misconfiguration on a 2020 web framework leaked some configuration data. CORS misconfiguration on a 2026 local AI agent UI leaks the entire developer machine plus production deploy capability.
The fix is not a patch. The fix is a class of controls: explicit path enumeration with tests, defense-in-depth header validation, capability sandboxing, and review discipline on agent-authored commits. Tools that ship one without the others ship CVE-2026-2611’s successor with a different prefix.
FAQ
What is CVE-2026-2611?
CVE-2026-2611 is the National Vulnerability Database identifier for an improper origin validation flaw in MLflow 3.9.0’s Assistant feature. The flaw lets any webpage make cross-origin requests to the Assistant’s local /ajax-api endpoint, modify its configuration to enable full-access mode, then execute arbitrary commands through the bundled Claude Code CLI integration. CVSS 3.0 base score 9.6 (CRITICAL). Fixed in MLflow 3.10.0 via PR #20832, “Block CORS for ajax paths.”12
What was the root cause?
MLflow’s is_api_endpoint() check enumerated only the /api/ path prefix. When the Assistant feature introduced /ajax-api/ endpoints, nobody updated the enumeration. The CORS-blocking middleware then waved cross-origin requests to /ajax-api/ through without any origin check. A second weakness compounded the bug: the legacy CORSMiddleware allowed allow_origins=["*"], so even non-blocked paths echoed permissive CORS headers back to any origin. The fix adds AJAX_API_PATH_PREFIX = "/ajax-api/" to the path check and replaces allow_origins=["*"] with the configured allowlist plus a localhost regex.2
How do I know if my MLflow installation is affected?
Run pip show mlflow | grep Version. MLflow versions >=3.9.0,<3.10.0 are affected per NVD’s CPE range. MLflow 3.10.0 (released 2026-02-20) ships the fix. Upgrade with pip install --upgrade mlflow and restart any running mlflow ui processes.
Does the attack work without the Claude Code CLI installed?
The configuration-modification step works regardless. The arbitrary-code-execution step requires the Claude Code CLI to be present on the victim’s PATH because MLflow’s Assistant invokes it via shutil.which("claude"). Without the CLI, the worst case becomes information disclosure and arbitrary configuration tampering rather than RCE. For users who configured the Claude Code backend (the documented Assistant setup path), the practical exposure is RCE.
How do I protect other local AI agent services?
Audit for the four ingredients: loopback or wildcard binding, mutation endpoints, agent-level capabilities, and any single boundary check that may have gaps. Add explicit Origin allowlists, Host header validation against 127.0.0.1/localhost (to defeat DNS rebinding), and Sec-Fetch-Site: same-origin gates on destructive endpoints. Sandbox the agent’s execution capability with bwrap, App Sandbox, or a per-workspace container. Treat the agent’s git commits as untrusted until reviewed.
-
National Vulnerability Database. “CVE-2026-2611: MLflow Assistant improper origin validation.” Published May 19, 2026. CVSS 3.0 base score 9.6 (CRITICAL), vector
CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H, issued by Huntr. Fix shipped in MLflow 3.10.0. https://nvd.nist.gov/vuln/detail/CVE-2026-2611. Original report: https://huntr.com/bounties/8462addd-b464-4a84-b6a2-5529604e6e5a. ↩↩↩ -
MLflow. “Block CORS for ajax paths (#20832).” Merged February 16, 2026 by Tomu Hirata. Files changed:
mlflow/server/fastapi_security.py(+5/-1),mlflow/server/security_utils.py(+5/-2), plus test updates. The diff replacesis_api_endpoint()’s single-prefix check with a two-prefix check and tightens the legacyCORSMiddleware’sallow_originsfrom["*"]to the configured allowlist plus a localhost regex. https://github.com/mlflow/mlflow/commit/8f9c8a53af90842944101eb8b7d60706822c81bc. ↩↩↩↩↩↩ -
MLflow 3.10.0 source.
mlflow/assistant/providers/claude_code.py:shutil.which("claude")lookup (lines 268, 280, 355); install hint"Install it with: npm install -g @anthropic-ai/claude-code"(line 286); permission-mode escalationcmd.extend(["--permission-mode", "bypassPermissions"])gated onconfig.permissions.full_access(lines 379–381).mlflow/assistant/config.py:PermissionsConfig.full_access: bool = Falsedefault (line 15).mlflow/server/assistant/api.py: router prefix/ajax-api/3.0/mlflow/assistant(line 63);GET /configreader (line 261);PUT /configmutator (line 276) — the PUT endpoint is the one the cross-origin attack targets to flipfull_access. Verified against the MLflow 3.10.0 sdist on PyPI (mlflow-3.10.0.tar.gz). ↩ -
W3C. “Fetch Metadata Request Headers.” Defines
Sec-Fetch-Siteand its valuescross-site,same-origin,same-site,none. Implemented by Chrome 76+, Firefox 90+, Safari 16.4+, and Edge 79+. https://www.w3.org/TR/fetch-metadata/. ↩