导语
安全补丁最容易产生一种错觉:漏洞编号消失了,风险也就归零了。
CVE-2026-59971 的修复为 MySQL MCP Server 的 SSE 传输启用了 DNS Rebinding 防护,校验请求的 Host 与 Origin。这一步非常必要,但项目文档同时明确提示:SSE 没有内置身份认证。
这不是矛盾,而是两个不同问题:来源校验判断请求是否从允许的网络来源进入,身份认证判断调用者是谁。即使请求来自合法域名,也可能是匿名用户;即使用户已经登录,也不代表其有权调用任意 SQL 工具。
本文以该漏洞为例,为远程 MCP 服务建立"来源---身份---能力---资源"四层模型。
一、漏洞事实边界
-
mysql-mcp-server < 0.4.2在 SSE 模式下缺少Host/Origin校验和路由认证。 -
默认 stdio 传输不受公告所述网络攻击路径影响。
-
GitHub 将 CVE-2026-59971 评为 Critical,CVSS 3.1 为 10.0。
-
修复版本
0.4.2于 2026 年 6 月 20 日发布,开启底层 MCP SDK 的 DNS Rebinding 防护。 -
当前文档仍说明 SSE 无内置认证,要求对外使用时置于带认证的代理之后。
-
公告于 6 月 21 日发布,直到 9 月 11 日才进入 GitHub 已审核漏洞数据库。
-
截至核验时没有一手来源确认在野利用;报告所述公开可达实例数量不应等同于受害者数量。
二、四层信任模型
第一层:来源
回答"请求从哪个 Host、Origin 和网络入口到达"。主要控制包括:
-
Host 允许列表;
-
Origin 允许列表;
-
监听地址与防火墙;
-
可信反向代理配置。
它可以降低 DNS Rebinding 和错误代理配置造成的来源混淆,但不能证明用户身份。
第二层:身份
回答"调用者是谁"。可以使用:
-
短期访问令牌;
-
mTLS 客户端证书;
-
OIDC/OAuth 代理;
-
企业 API Gateway 身份。
认证应覆盖会话建立与消息提交的完整路径。只保护 /sse 或只保护 /messages/ 都可能留下策略缺口。
第三层:能力
回答"这个身份可以调用哪些工具"。工具不应只有"可见/不可见"两种状态,还应区分:
-
列出元数据;
-
读取限定视图;
-
修改指定数据;
-
执行任意 SQL。
客户端显示的 destructiveHint 只是提示,不是服务端授权。
第四层:资源
回答"工具连接的后端账号最终能影响什么"。即便 MCP 层授权正确,若所有用户共享一个数据库管理员账号,后端仍无法约束越权范围。
完整判定应该是:
allowed_source(request)
AND authenticated(identity)
AND authorized(identity, tool)
AND permitted(db_account, object, operation)
缺少任何一层,都可能把上层小失误放大为真实数据风险。
三、为什么只做 Origin 校验仍不够
假设反向代理合法域名是 mcp.example.com,升级后它已加入允许 Host。此时下面两类请求在来源层看起来相同:
合法员工客户端 → mcp.example.com
匿名互联网客户端 → mcp.example.com
如果没有身份认证,来源校验无法区分二者。
同样,认证后的普通分析人员与数据库管理员都可能从同一域名访问:
分析人员 → execute_sql(只读查询)
管理员 → execute_sql(维护操作)
如果工具层没有角色授权,身份认证也无法限制能力。
因此,来源校验是必要条件,不是充分条件。
四、无害访问控制模型
下面的代码不启动服务、不连接 MySQL,仅验证策略顺序:
from dataclasses import dataclass
@dataclass(frozen=True)
class Request:
host: str
user: str | None
roles: frozenset[str]
def can_call(request: Request, tool: str) -> bool:
if request.host != "mcp.example.com":
return False
if request.user is None:
return False
policy = {
"list_tables": {"reader", "operator"},
"sample_table": {"reader", "operator"},
"execute_sql": {"operator"},
}
return bool(request.roles & policy.get(tool, set()))
assert not can_call(Request("evil.example", None, frozenset()), "list_tables")
assert not can_call(Request("mcp.example.com", None, frozenset()), "list_tables")
assert can_call(Request("mcp.example.com", "alice", frozenset({"reader"})), "list_tables")
assert not can_call(Request("mcp.example.com", "alice", frozenset({"reader"})), "execute_sql")
这四个断言分别验证:来源非法被拒绝、匿名请求被拒绝、读者可以使用只读工具、读者不能调用高影响工具。
生产系统还应把工具参数映射到数据库对象与操作类型,不能仅在工具名称层授权。
五、补丁与剩余风险
官方补丁通过 TransportSecuritySettings 启用 DNS Rebinding 防护,并设置允许 Host。它解决了 CVE 所描述的重要根因。
但补丁没有给 SSE 增加身份体系,项目 README 明确建议:
-
SSE 进程绑定
127.0.0.1; -
由 nginx、Caddy、Traefik 等代理提供外部入口;
-
在代理层强制认证;
-
正确配置转发后的 Host。
此外,补丁对缺少 transport_security 的旧 MCP SDK 采用兼容性回退。当前项目依赖范围仍允许 mcp>=1.2.0,<2,而补丁日志称相关能力需要 mcp>=1.9.0。部署方应核验运行时版本和启动日志,不能只验证顶层包版本。
这一段属于基于官方代码的工程风险分析,并非新的 CVE 结论。
六、开发与安全团队行动清单
P0:封闭匿名入口
-
升级至
0.4.2+,优先使用当前0.4.4。 -
非必要关闭 SSE,使用 stdio。
-
SSE 绑定 loopback,通过统一代理访问。
-
对
/sse、/messages/和健康检查之外的业务路由应用一致认证。 -
为服务身份设置短期凭据、轮换与撤销机制。
-
删除 MCP 数据库账号的管理员权限和
FILE权限。
P1:落实工具级授权
-
默认不向普通身份开放任意 SQL。
-
把只读查询封装为限定视图或固定模板。
-
写操作使用独立角色、独立连接池和审批策略。
-
在服务端验证角色,而不是依赖客户端隐藏工具。
-
记录身份、会话、工具、参数摘要、数据库对象和结果状态。
P2:自动化验收
构建负向测试矩阵:
| 场景 | 预期结果 |
|---|---|
| 非允许 Host | 拒绝 |
| 合法 Host、无身份 | 拒绝 |
| 合法身份、无工具权限 | 拒绝 |
| 只读身份发起写操作 | 拒绝 |
| SDK 安全能力缺失 | 启动失败 |
| 代理遗漏消息路径认证 | 发布阻断 |
将这些测试放在镜像验收和部署后探测中,而不是仅靠代码审查。
七、事件排查建议
如曾运行受影响 SSE 模式,应关联三类日志:
-
代理或应用访问日志:Host、Origin、来源 IP、长连接和消息端点调用;
-
MCP 审计:会话建立、工具调用、身份与拒绝原因;
-
MySQL 审计:跨库访问、大批量读取、异常 DML、权限枚举和文件操作。
若系统过去完全没有身份日志,就无法把 SQL 操作可靠归因到具体用户。这本身就是需要整改的证据,而不是"没有发现异常"。
总结
CVE-2026-59971 的补丁告诉我们怎样拦截来源混淆;它的剩余边界则提醒我们,来源合法不代表调用者可信,身份可信也不代表拥有全部工具权限。
远程 MCP 服务的正确安全顺序应是:先验证来源,再验证身份,然后授权工具,最后由后端账号限制真实资源。任何一步依赖前一步"顺便兜底",都会在系统演进后变成漏洞。