CVE-2025-14279: MLflow DNS Rebinding 漏洞分析

一、漏洞概述

1.1 漏洞简介

MLflow 是一个开源的机器学习生命周期管理平台,由 Databricks 开发。该漏洞存在于 MLflow REST Server 中,由于缺乏 Origin/Host 头验证,导致服务器容易受到 DNS Rebinding 攻击。

攻击者可以通过恶意网站绕过浏览器的同源策略 (Same-Origin Policy) 保护,对 MLflow REST API 端点执行未授权调用,从而实现对实验数据的查询、更新和删除操作,可能导致数据泄露、数据破坏或数据篡改。

二、DNS Rebinding 攻击原理

2.1 什么是 DNS Rebinding?

DNS Rebinding 是一种利用 DNS 解析机制绕过浏览器同源策略的攻击技术。攻击的核心思想是:

  1. 攻击者控制一个域名(如 evil.com)及其 DNS 服务器
  2. 第一次 DNS 查询返回攻击者服务器的 IP(如 1.2.3.4)
  3. 受害者浏览器加载攻击者的恶意页面
  4. 攻击者将 DNS 记录更改为目标内网 IP(如 127.0.0.1 或 192.168.1.100)
  5. 当恶意 JavaScript 发起请求时,浏览器认为仍在访问 evil.com(同源),但实际请求发送到了内网服务

2.2 攻击流程图

┌─────────────────────────────────────────────────────────────────────┐
│                     DNS Rebinding 攻击流程                           │
└─────────────────────────────────────────────────────────────────────┘

  ┌──────────┐                ┌──────────┐              ┌──────────────┐
  │  受害者   │                │ 攻击者   │              │  MLflow      │
  │  浏览器   │                │ DNS/Web  │              │  Server      │
  └────┬─────┘                └────┬─────┘              │ (127.0.0.1)  │
       │                           │                    └──────┬───────┘
       │  1. 访问 evil.com         │                           │
       │ ─────────────────────────>│                           │
       │                           │                           │
       │  2. DNS 返回 1.2.3.4      │                           │
       │ <─────────────────────────│                           │
       │                           │                           │
       │  3. 加载恶意页面           │                           │
       │ <─────────────────────────│                           │
       │                           │                           │
       │  [恶意 JS 等待 DNS TTL 过期]                           │
       │                           │                           │
       │  4. 再次查询 evil.com DNS  │                           │
       │ ─────────────────────────>│                           │
       │                           │                           │
       │  5. DNS 返回 127.0.0.1 ⚠️  │                           │
       │ <─────────────────────────│                           │
       │                           │                           │
       │  6. JS 发送请求到 evil.com │                           │
       │   (浏览器认为是同源)       │                           │
       │ ─────────────────────────────────────────────────────>│
       │                           │                           │
       │  7. MLflow 返回敏感数据    │                           │
       │ <─────────────────────────────────────────────────────│
       │                           │                           │
       │  8. JS 将数据发送给攻击者  │                           │
       │ ─────────────────────────>│                           │
       │                           │                           │

2.3 为什么 MLflow 受影响?

在 MLflow 3.4.0 及之前的版本中:

  1. 缺乏 Host 头验证:服务器不验证请求的 Host 头是否合法
  2. 缺乏 Origin 验证:不检查请求的来源是否在允许列表中
  3. 无 CORS 保护:允许任意来源的跨域请求访问 API

这使得攻击者可以通过 DNS Rebinding 绕过浏览器的同源策略,对运行在 localhost 或内网的 MLflow 服务发起攻击。

三、漏洞原理详解

3.1 漏洞根因分析

漏洞版本代码 (MLflow ≤ 3.4.0)

在漏洞版本中,MLflow 的 FastAPI 应用直接暴露 REST API,没有任何安全中间件保护:


四、漏洞环境搭建

4.1 环境要求

  • Python 3.8+
  • MLflow 3.4.0 (漏洞版本)
  • 现代浏览器 (Chrome/Firefox)
  • 可选:自建 DNS 服务器用于 DNS Rebinding

4.2 安装漏洞版本的 MLflow

# 创建虚拟环境
python -m venv mlflow_vuln_env
cd mlflow_vuln_env

# Windows
.\Scripts\activate

# Linux/Mac
source bin/activate

# 安装漏洞版本
pip install mlflow==3.4.0

4.3 启动 MLflow Server

# 使用 0.0.0.0 以便 DNS Rebinding 攻击可以访问
mlflow server --host 127.0.0.1 --port 5000

4.4 DNS Rebinding 测试域名 (rbndr.us)

本文使用 rbndr.us 服务进行 DNS Rebinding 测试,无需自建 DNS 服务器。

域名格式: <IP1_HEX>.<IP2_HEX>.rbndr.us

测试域名: 72727272.7f000001.rbndr.us

组成部分

十六进制

IP 地址

说明

第一段

72727272

114.114.114.114

公网 DNS 服务器 IP

第二段

7f000001

127.0.0.1

本地回环地址

工作原理:

  • DNS 服务器会在两个 IP 之间随机切换
  • 首次解析可能返回 114.114.114.114
  • 再次解析可能返回 127.0.0.1
  • 浏览器认为是"同源"请求,实际访问了本地服务

验证 DNS Rebinding:

# 多次执行,观察 IP 变化
nslookup 72727272.7f000001.rbndr.us

五、漏洞复现

5.1 使用 rbndr.us 进行DNS Rebinding 攻击

使用 72727272.7f000001.rbndr.us 域名进行真实的 DNS Rebinding 攻击复现。

这里使用get请求进行测试

六、漏洞修复分析

6.1 官方修复 Commit

修复 Commit: b0ffd289e9b0d0cc32c9e3a9b9f3843ae83dbec3

6.2 修复方案概述

MLflow 3.5.0 添加了一个完整的安全中间件层,包括:

  1. Host 头验证 (DNS Rebinding 防护)
  2. CORS 保护
  3. X-Frame-Options 头 (防止点击劫持)
  4. 安全响应头

6.3 新增安全配置选项

配置项

CLI 选项

环境变量

默认值

允许的 Host

--allowed-hosts

MLFLOW_SERVER_ALLOWED_HOSTS

localhost, 私有 IP

CORS 来源

--cors-allowed-origins

MLFLOW_SERVER_CORS_ALLOWED_ORIGINS

localhost:*

X-Frame-Options

--x-frame-options

MLFLOW_SERVER_X_FRAME_OPTIONS

SAMEORIGIN

禁用安全中间件

--disable-security-middleware

MLFLOW_SERVER_DISABLE_SECURITY_MIDDLEWARE

false

6.5 核心补丁代码分析

6.5.1 mlflow/server/fastapi_security.py - 安全中间件核心

这是补丁的核心文件

# mlflow/server/fastapi_security.py (MLflow 3.5.0+)
# 来源: https://github.com/mlflow/mlflow/commit/b0ffd289e9b0d0cc32c9e3a9b9f3843ae83dbec3

from starlette.middleware.cors import CORSMiddleware


class HostValidationMiddleware:
    """
    Host 头验证中间件 - 防止 DNS Rebinding 攻击
    
    通过验证 HTTP 请求的 Host 头是否在允许列表中,
    阻止来自恶意域名的请求访问内部服务。
    """
    
    def __init__(self, app, allowed_hosts: list[str]):
        self.app = app
        self.allowed_hosts = allowed_hosts
        # 编译通配符模式为正则表达式
        self.allowed_host_patterns = [
            self._compile_pattern(host) for host in allowed_hosts
        ]
    
    def _compile_pattern(self, pattern: str):
        """将通配符模式编译为正则表达式"""
        import re
        # 支持 *.example.com 和 localhost:* 格式
        regex = pattern.replace(".", r"\.").replace("*", r".*")
        return re.compile(f"^{regex}$", re.IGNORECASE)
    
    async def __call__(self, scope, receive, send):
        if scope["type"] != "http":
            await self.app(scope, receive, send)
            return
        
        # 提取 Host 头
        headers = dict(scope["headers"])
        host = headers.get(b"host", b"").decode("utf-8")
        
        # ✅ 关键修复:验证 Host 头是否在允许列表中
        if not self._is_valid_host(host):
            # 返回 400 Bad Request,拒绝可疑请求
            response = Response(
                content="Invalid Host header",
                status_code=400
            )
            await response(scope, receive, send)
            return
        
        await self.app(scope, receive, send)
    
    def _is_valid_host(self, host: str) -> bool:
        """检查 Host 是否匹配允许的模式"""
        # 移除端口号进行匹配
        host_without_port = host.split(":")[0] if ":" in host else host
        
        for pattern in self.allowed_host_patterns:
            if pattern.match(host) or pattern.match(host_without_port):
                return True
        
        return False


class CORSBlockingMiddleware:
    """
    CORS 阻止中间件 - 防止跨域攻击
    
    验证 Origin 头是否在允许列表中,
    阻止恶意网站的跨域请求。
    """
    
    STATE_CHANGING_METHODS = {"POST", "PUT", "DELETE", "PATCH"}
    
    def __init__(self, app, allowed_origins: list[str]):
        self.app = app
        self.allowed_origins = allowed_origins
        self.allow_all = "*" in allowed_origins
    
    async def __call__(self, scope, receive, send):
        if scope["type"] != "http":
            await self.app(scope, receive, send)
            return
        
        headers = dict(scope["headers"])
        origin = headers.get(b"origin", b"").decode("utf-8")
        method = scope.get("method", "GET")
        
        # ✅ 关键修复:对状态变更请求验证 Origin
        if origin and method in self.STATE_CHANGING_METHODS:
            if not self._is_allowed_origin(origin):
                response = Response(
                    content="CORS request blocked",
                    status_code=403
                )
                await response(scope, receive, send)
                return
        
        await self.app(scope, receive, send)
    
    def _is_allowed_origin(self, origin: str) -> bool:
        """检查 Origin 是否被允许"""
        if self.allow_all:
            return True
        
        return any(
            self._match_origin(origin, allowed)
            for allowed in self.allowed_origins
        )


class SecurityHeadersMiddleware:
    """
    安全响应头中间件 - 防止点击劫持和 XSS
    
    自动添加安全相关的 HTTP 响应头。
    """
    
    def __init__(self, app, x_frame_options: str = "SAMEORIGIN"):
        self.app = app
        self.x_frame_options = x_frame_options
    
    async def __call__(self, scope, receive, send):
        if scope["type"] != "http":
            await self.app(scope, receive, send)
            return
        
        async def send_wrapper(message):
            if message["type"] == "http.response.start":
                headers = dict(message.get("headers", []))
                
                # ✅ 添加安全响应头
                headers[b"x-content-type-options"] = b"nosniff"
                headers[b"x-frame-options"] = self.x_frame_options.encode()
                headers[b"x-xss-protection"] = b"1; mode=block"
                
                message["headers"] = list(headers.items())
            
            await send(message)
        
        await self.app(scope, receive, send_wrapper)


def configure_security_middleware(app, config):
    """
    配置安全中间件层 - MLflow 3.5.0+ 的核心修复
    
    按正确顺序添加所有安全中间件。
    中间件执行顺序(后添加的先执行):
    1. SecurityHeadersMiddleware - 添加安全响应头
    2. CORSMiddleware - 处理 CORS 预检请求
    3. CORSBlockingMiddleware - 阻止非法跨域请求
    4. HostValidationMiddleware - 验证 Host 头
    """
    
    if config.disable_security_middleware:
        _logger.warning(
            "Security middleware is disabled. "
            "Only use this when security is handled by a reverse proxy."
        )
        return
    
    allowed_hosts = config.get_allowed_hosts()
    allowed_origins = config.get_allowed_origins()
    
    # ✅ 添加安全响应头中间件
    app.add_middleware(
        SecurityHeadersMiddleware,
        x_frame_options=config.x_frame_options
    )
    
    # ✅ 添加 CORS 中间件 (Starlette 标准中间件)
    app.add_middleware(
        CORSMiddleware,
        allow_origins=allowed_origins,
        allow_credentials=True,
        allow_methods=["*"],
        allow_headers=["*"],
    )
    
    # ✅ 添加 CORS 阻止中间件 (自定义,处理状态变更请求)
    app.add_middleware(
        CORSBlockingMiddleware,
        allowed_origins=allowed_origins
    )
    
    # ✅ 添加 Host 头验证中间件
    app.add_middleware(
        HostValidationMiddleware,
        allowed_hosts=allowed_hosts
    )
    
    _logger.info(f"Security middleware configured:")
    _logger.info(f"  Allowed hosts: {allowed_hosts}")
    _logger.info(f"  Allowed CORS origins: {allowed_origins}")
    _logger.info(f"  X-Frame-Options: {config.x_frame_options}")

6.5.2 mlflow/server/fastapi_app.py - FastAPI 应用集成

补丁修改了 FastAPI 应用创建逻辑,集成安全中间件:

# mlflow/server/fastapi_app.py (补丁修改部分)

from mlflow.server.fastapi_security import configure_security_middleware

def create_app(config):
    """Create FastAPI application with security middleware."""
    app = FastAPI(
        title="MLflow Tracking Server",
        ...
    )
    
    # ✅ 补丁新增:配置安全中间件
    if not config.disable_security_middleware:
        configure_security_middleware(
            app,
            allowed_hosts=config.allowed_hosts,
            cors_allowed_origins=config.cors_allowed_origins,
            x_frame_options=config.x_frame_options,
        )
    else:
        _logger.warning(
            "Security middleware is disabled. "
            "Ensure security is handled by your reverse proxy."
        )
    
    # Mount Flask app for backward compatibility
    app.mount("/", WSGIMiddleware(flask_app))
    
    return app

七、总结

CVE-2025-14279 是一个典型的 DNS Rebinding 漏洞,其根本原因在于:

  1. 缺乏 Host 头验证:服务器接受任意 Host 头,无法防御 DNS Rebinding
  2. CORS 配置不当:允许任意来源的跨域请求
  3. 无安全响应头:缺少必要的安全 HTTP 响应头

该漏洞影响 MLflow 3.4.0 及之前的所有版本,攻击者可以通过恶意网站对运行在本地或内网的 MLflow 服务发起未授权访问,窃取或篡改机器学习实验数据。

八、参考链接