你在本机跑了一个 Go MCP 服务器给 Claude Code 用,没认证、绑在 127.0.0.1,觉得很安全。但一个你随手打开的网页,可能正在直接给你的 MCP 服务器发
tools/call。
Go MCP SDK CVE-2026-34742:localhost 服务器默认裸奔
一、事件速览
| 项目 | 内容 |
|---|---|
| CVE 编号 | CVE-2026-34742 |
| 影响组件 | 官方 modelcontextprotocol/go-sdk(Model Context Protocol 的 Go 语言官方实现) |
| 漏洞类型 | HTTP 传输模式下默认未启用 DNS rebinding 防护(CWE-350,Host 头信任缺失) |
| 受影响条件 | HTTP 服务器 + 运行在 localhost + 无认证 + 使用 StreamableHTTPHandler 或 SSEHandler |
| 攻击效果 | 恶意网页绕过浏览器同源策略,冒充用户调用 MCP 工具、读取暴露的资源 |
| CVSS 4.0 | CVSS-B 7.6 HIGH(AV:N/AC:L/AT:P/PR:N/UI:P/VC:H/VI:H/VA:N;后续系统影响 SC:N/SI:N/SA:N) |
| 修复版本 | 1.4.0 |
| 修复 commit | 67bd3f2(PR #760,mcp: add automatic DNS rebinding protection for localhost servers) |
按 CVSS 4.0 的定义逐项看:AC:L 攻击复杂度低,但 AT:P(Attack Requirements: Present)表示攻击依赖特定的前置条件——受害者本机得跑着一个无认证的 localhost MCP 服务器;UI:P(User Interaction: Passive)表示需要用户被动交互,即访问一次恶意网页即可,无需更多操作。这正是 DNS rebinding 类攻击的典型画像:不攻破服务器本身,而是借道受害者的浏览器。
二、DNS rebinding:同源策略的经典盲区
先补 30 秒原理。浏览器的同源策略(SOP)以"源"(协议 + 域名 + 端口)为单位隔离内容,而不是以 IP 为单位。DNS rebinding 攻击利用的正是这个缝隙:
文字版攻击时序
─────────────────────────────────────────────────────
[1] 受害者访问 evil.com
浏览器请求 DNS → evil.com 返回 1.2.3.4(攻击者服务器)
[2] 攻击者把 1.2.3.4 的 TTL 设得极短(如 1 秒)
页面加载完成后,浏览器很快丢弃这条 DNS 记录
[3] 页面里的 JS 再次请求 evil.com 上的"API 路径"
浏览器重新做 DNS 解析 → 这次返回 127.0.0.1
[4] 请求实际打到受害者本机的服务上
但在浏览器看来,源仍是 evil.com —— 同源,不拦截
[5] 本机服务的响应也回到 evil.com 的 JS 手里
(攻击者甚至能读到响应)
─────────────────────────────────────────────────────为什么这招对 MCP 服务器特别有效?MCP 规范的安全最佳实践(2025-11-25 版 spec 的 security_best_practices#local-mcp-server-compromise 一节)专门警告过这个场景:本地 MCP 服务器通常:
- 绑定 127.0.0.1——开发者以为"只有本机能访问";
- 不做认证——反正是自己机器上的进程在调用;
- 拥有高权限工具——文件读写、命令执行、数据库查询,正是 MCP 的价值所在。
三条叠加之后,一个能向 localhost 发任意 HTTP 请求的恶意网页,等于拿到了一把免认证的工具调用钥匙。CVE 描述里写得很直白:攻击者可以"invoke tools or access resources exposed by the MCP server on behalf of the user"。
和 Serena 漏洞是什么关系
你可能在站内读过 Serena MCP 的 DNS rebinding RCE(CVE-2026-49471)。两者同根同源但层次不同:Serena 是一个具体项目被 rebinding 打穿并升级到 RCE;CVE-2026-34742 是官方 SDK 层面的默认配置缺陷——意味着所有基于官方 Go SDK 起的 localhost HTTP 服务器都裸奔,而不仅仅是某一家。SDK 层的修复一次覆盖整个生态,这也是它值得单独写一篇的原因。
三、修复方案拆解:PR #760 怎么做的
修复 commit 67bd3f2 共 5 个文件、+203/-2 行,作者 pcarleton 与 maciej-kisiel(Google)。PR 描述里有句实话:这是 Claude 辅助完成的 PR,目的是"看看通过这套一致性测试对这个 SDK 来说有多难"。结果通过得很干脆。
3.1 设计目标:Secure by Default,且难以被误关
PR 明确列出了三个取舍:
- 零代码改动:已有服务器自动获得防护;
- 无需 opt-in:防护基于连接的本地地址在运行时激活;
- 显式 opt-out:想关必须主动设置
DisableLocalhostProtection: true。
为什么不做成一个"辅助函数"让开发者自己包一层?PR 里讨论过其他能提供安全默认值的路径(比如给 ListenAndServe 加一个做 localhost 检查的 helper),但结论是:用 http.LocalAddrContextKey 做运行时探测是最向后兼容、也最不可能被意外禁用的做法。
3.2 核心机制:LocalAddr + Host 双重校验
文字版逻辑链:
请求进入 StreamableHTTPHandler.ServeHTTP
│
▼
[1] 读 http.LocalAddrContextKey —— 这条连接的本地地址是什么?
│
├── 非 localhost(如外部网卡请求)→ 不加防护,直接放行
│ (外部请求自有防火墙/认证体系管,SDK 不越俎代庖)
│
└── 是 localhost(127.0.0.1 / [::1])
│
▼
[2] 校验 Host 头是否也是 localhost 形态
│
├── Host: 127.0.0.1 / localhost / [::1] → 放行
│
└── Host: evil.com 等 → 403 Forbidden关键洞察是:DNS rebinding 攻击必须通过浏览器发起,而浏览器请求 localhost 服务时带的 Host 头就是域名本身(如 Host: evil.com)。正常本机客户端访问 127.0.0.1:8080 时,Host 头自然是 127.0.0.1:8080。所以"连接来自 localhost + Host 头不是 localhost"这个组合,几乎只可能是 rebinding。
这段防护在 StreamableHTTPHandler.ServeHTTP 的最前面执行(早于跨域检查、体积限制、协议版本检查)。当前 mcp/streamable.go 中的真实实现是:
// DNS rebinding protection: auto-enabled for localhost servers.
// See: https://modelcontextprotocol.io/specification/2025-11-25/basic/security_best_practices#local-mcp-server-compromise
if !h.opts.DisableLocalhostProtection {
if localAddr, ok := req.Context().Value(http.LocalAddrContextKey).(net.Addr); ok && localAddr != nil {
if util.IsLoopback(localAddr.String()) && !util.IsLoopback(req.Host) {
http.Error(w, fmt.Sprintf("Forbidden: invalid Host header %q", req.Host), http.StatusForbidden)
return
}
}
}配套的选项字段与注释(StreamableHTTPOptions):
// DisableLocalhostProtection disables automatic DNS rebinding protection.
// By default, requests arriving via a localhost address (127.0.0.1, [::1])
// that have a non-localhost Host header are rejected with 403 Forbidden.
// This protects against DNS rebinding attacks regardless of whether the
// server is listening on localhost specifically or on 0.0.0.0.
//
// Only disable this if you understand the security implications.
DisableLocalhostProtection bool实现细节值得记一笔:
- 类型断言是
net.Addr而非*net.TCPAddr——不假设底层一定是 TCP,localAddr.String()得到127.0.0.1:8080这类含端口字符串; - 两种形态的判断都交给
util.IsLoopback(string)——它同时处理服务器地址(带端口)与 Host 头(可能带端口、也可能是localhost这种非 IP 字面量),所以作者不必手写SplitHostPort; - 拒绝时返回的响应体带上下文:
Forbidden: invalid Host header "evil.com"。 - PR #760 的描述里提到新增
isLocalhostAddr/isLocalhostHost两个辅助函数;当前代码已把这项职责统一收敛到internal/util包的IsLoopback。
注意实现层面的两个细节:
- 用
LocalAddrContextKey而不是解析r.Host的端口——防护针对"这条连接到底从哪来",即使服务器绑在0.0.0.0,只要请求是从 loopback 进来的就启用校验;反过来,从局域网进来的请求完全不受影响。 - Host 头要兼容带端口与不带端口两种形态,
net.SplitHostPort失败时退回原值,net.ParseIP判 loopback。
3.3 一致性测试:这不是 Go SDK 的独角戏
PR 附带的验证不只跑单测,还过了 MCP 官方 conformance 测试套件的 dns-rebinding-protection 场景:
localhost-host-rebinding-rejected:PASSlocalhost-host-valid-accepted:PASS
配套的另一块拼图是 TypeScript SDK 的同名实现——localhostHostValidation() 中间件。也就是说,DNS rebinding 防护是规范级要求 + 官方 SDK 统一落地,两个主要语言 SDK 行为对齐。写跨语言 MCP 组件时可以预期一致的安全基线。
四、影响面与自查清单
4.1 你受影响吗
同时满足以下条件才有实际风险:
- 使用官方 Go SDK 的
StreamableHTTPHandler或SSEHandler(即 HTTP/SSE 传输); - 版本 < 1.4.0;
- 服务器监听 localhost 且无认证;
- 用户会用浏览器访问任意网页(这一条基本无法避免)。
stdio 传输(StdioTransport)的 MCP 服务器与此无关——没有 HTTP 面,rebinding 无从谈起。
4.2 升级与配置
# 升级到 1.4.0+,防护自动生效
go get github.com/modelcontextprotocol/go-sdk@v1.4.0正常情况下你不需要改任何代码。唯一的例外是同机反向代理场景:
浏览器 → nginx (本机) → 127.0.0.1:8080 (Go MCP server)
│
└── 代理保留原始 Host 头(proxy_set_header Host $host;)
→ MCP server 收到的 Host 头是外部域名
→ 新版防护会 403 拒绝!两种解法(PR 原文给出的正是这两条):
// 方案 A:关掉防护(确认代理层有其他防护措施时)
handler := mcp.NewStreamableHTTPHandler(serverFn, &mcp.StreamableHTTPOptions{
DisableLocalhostProtection: true,
})
// 方案 B:让代理改写 Host 头为 localhost(推荐)
// nginx: proxy_set_header Host 127.0.0.1:8080;方案 B 更符合纵深防御:代理本来就应该把"内部服务"的身份传递给后端,而不是把外部域名原样透传。
4.3 即使升级了,别停在这里
Host 校验挡住的是 rebinding 这一条路。本地 MCP 服务器的完整加固清单还包括:
- 尽量绑
127.0.0.1而非0.0.0.0——0.0.0.0会把服务暴露给整个局域网,等价于把免认证的 MCP 面开放给同网段所有设备(参考站内微软 UFO Mobile MCP 无认证接管的教训:0.0.0.0绑定是 CVSS 9.4 漏洞的触发器); - 给 HTTP 传输加一层认证,哪怕是简单的 token 校验;
- 工具面最小化:不用的工具别注册,文件系统工具限制根目录;
- 服务器审计日志记下每一次
tools/call的来源。
五、几个值得琢磨的细节
其一,"Claude 辅助 PR" 出现在官方安全修复里。 PR 描述明说这是 Claude 辅助完成、用来测试"实现难度"的 PR。一个安全默认值的落地成本,低到可以用 AI 快速验证——这本身就是 MCP 生态工程节奏的一个信号。conformance 测试全部通过(localhost-host-rebinding-rejected / localhost-host-valid-accepted),这是合并的硬门槛。
其二,防护的射程被刻意限定在"经由 localhost 到达的请求"。 PR 明确说明:从非 localhost IP(如外部网络请求)进来的请求不受影响,无论服务器监听的是 127.0.0.1 还是 0.0.0.0。这个边界划得很克制——SDK 只在攻击特征最明确的场景(loopback 连接 + 非 loopback Host 头)出手,不去干涉公网部署、反向代理、域前置这些形态各异的合法拓扑。改动越小,越不会被误关。
其三,CVSS 的 AT:P 和 UI:P 不是在给漏洞"打折"。 7.6 分背后是 VC:H/VI:H(机密性、完整性影响都是高)——MCP 工具调用能做的事,就是漏洞的影响。AT:P 意味着需要"本机跑着无认证 localhost MCP 服务器"这个前置条件,UI:P 只要求受害者访问一次网页。对攻击者而言,这两个条件在 MCP 普及的今天几乎是默认成立的。
六、参考资料
- NVD — CVE-2026-34742:https://nvd.nist.gov/vuln/detail/CVE-2026-34742
- 修复 commit(PR #760):https://github.com/modelcontextprotocol/go-sdk/commit/67bd3f2e2b53ce11a16db8d976cdb8ff1e986b6d
- Red Hat 安全公告 RHSA-2026:21772:https://access.redhat.com/errata/RHSA-2026:21772
- MCP 规范 · Security Best Practices(Local MCP Server Compromise):https://modelcontextprotocol.io/specification/2025-11-25/basic/security_best_practices
- 站内相关:Serena MCP DNS rebinding RCE(CVE-2026-49471) · 微软 UFO Mobile MCP 无认证接管(CVE-2026-73296) · AWS 官方 MCP 服务器命令注入(CVE-2026-87911)

