FlawOpen

flawopen.com/AI Security/mlflow-webhook-ssrf-cve-2026-64849

● CVE-2026-64849 · CVSS 8.6 · 高危
安全研究 · FlawOpen

CVE-2026-64849:MLflow Webhook SSRF 漏洞

CVE-2026-64849 是 MLflow Webhook 发送功能中的未认证服务端请求伪造(SSRF) (高危,CVSS 3.1:8.6)。初始 URL 检查没有限制重定向目标,也没有约束实际连接使用的地址;测试接口可能将上游响应正文返回给调用者。[1]

💡 通俗易懂的原理解析 (ELI5)

想象前台检查了包裹上的地址,却让快递员按转寄便条前往新地址而不再核验。MLflow 在连接前检查了 Webhook URL,但重定向和第二次 DNS 查询仍可能把请求送到别处。修复会在发送 HTTP 数据前检查实际连接的对端地址。[1], [2]

核心概念与专有名词

SSRF
服务器代替调用方发起其选择的网络请求,从而跨越服务器自身的网络信任边界。[1]
Webhook 测试接口
向已配置 Webhook 发送测试事件的 MLflow API;受影响的默认服务器会把上游响应正文返回给调用者。[1]
DNS rebinding / TOCTOU
主机名在验证时解析为公网地址,却在连接时解析为另一个非公网地址。[1], [2]

框架映射

攻击执行流程分解

步骤 1

可访问的 MLflow 服务器接受 Webhook 测试

公告描述的是默认且未认证的 Tracking Server。认证插件和部署控制会改变可达性;应核实实际配置。 CISA 于 2026 年 8 月 19 日将该 CVE 加入 Known Exploited Vulnerabilities 目录;这表示已有已知利用情况,但不能证明某个具体部署已遭入侵。[1], [4], [5]

步骤 2

初始 URL 通过验证

_validate_webhook_url 解析并检查原始主机名,但丢弃已检查的地址,没有将其绑定到后续 socket。[1]

步骤 3

重定向或再次解析连接到其他目标

HTTP 客户端会跟随重定向,连接时也可能再次解析主机名。测试接口会反射上游响应正文,因此形成可读取完整响应的 SSRF。[1]

步骤 4

上游修复检查已连接对端

PR #24258 在连接后、TLS/HTTP 数据发送前检查 socket 对端;该会话也避免使用环境代理。MLflow 3.15.0 修复此 CVE;独立的 IPv6 转换绕过问题在 3.16.0 修复。 该公告将 freeman-bb 列为最初的私下报告者,并将 AUTHENSOR 列为独立发现者。[1], [2], [3], [6]

源代码对比:漏洞与安全实现

代码片段展示 URL 检查与实际请求之间的边界,以及上游修复在连接时检查对端。未包含利用请求和内部目标。3.16.0 的 IPv6 规范化是之后单独的加固措施。[2]

存在漏洞的实现
# PR #24258 之前 mlflow/webhooks/delivery.py 中的代码片段。
_validate_webhook_url(webhook.url)  # checks the initial URL only
return session.post(
    webhook.url, data=payload_bytes, headers=headers, timeout=timeout
)  # redirects follow by default; the validated IP is not pinned
加固后的安全修复
# 上游连接时校验对端的修复代码片段。
def _assert_public_peer(sock):
    if _MLFLOW_WEBHOOK_ALLOW_PRIVATE_IPS.get():
        return
    try:
        peer_ip = sock.getpeername()[0]
    except OSError as exc:
        sock.close()
        raise SSRFProtectionError(
            f"Could not determine webhook connection peer address: {exc}"
        ) from exc
    try:
        ip = ipaddress.ip_address(peer_ip)
    except ValueError as exc:
        sock.close()
        raise SSRFProtectionError(
            f"Webhook connection resolved to an invalid IP address: {peer_ip!r}"
        ) from exc
    if not ip.is_global:
        sock.close()
        raise SSRFProtectionError(
            f"Webhook connection blocked: {ip} is not a public IP address. "
            "This may indicate a DNS rebinding attempt."
        )


class _SSRFProtectedHTTPConnection(HTTPConnection):
    def _new_conn(self):
        sock = super()._new_conn()
        _assert_public_peer(sock)
        return sock


class _SSRFProtectedHTTPSConnection(HTTPSConnection):
    def _new_conn(self):
        sock = super()._new_conn()
        _assert_public_peer(sock)
        return sock

adapter = SSRFProtectedHTTPAdapter(max_retries=retry_strategy)
session = requests.Session()
session.trust_env = False
session.mount("http://", adapter)
session.mount("https://", adapter)

工程与系统安全加固清单

参考来源

← AI Security Directory →