Announcement

👇Official Account👇

Welcome to join the group & private message

Article first/tail QR code

Skip to content

你在本机跑了一个 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 + 无认证 + 使用 StreamableHTTPHandlerSSEHandler
攻击效果恶意网页绕过浏览器同源策略,冒充用户调用 MCP 工具、读取暴露的资源
CVSS 4.0CVSS-B 7.6 HIGH(AV:N/AC:L/AT:P/PR:N/UI:P/VC:H/VI:H/VA:N)
修复版本1.4.0
修复 commit67bd3f2(PR #760,mcp: add automatic DNS rebinding protection for localhost servers

CVSS 向量里有两个值得注意的修饰位:AT:P(攻击复杂度高——需要受害者浏览器处于可被 rebinding 的状态)和 UI:P(需要用户交互——受害者得访问攻击者的网页)。这正是 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 服务器通常:

  1. 绑定 127.0.0.1——开发者以为"只有本机能访问";
  2. 不做认证——反正是自己机器上的进程在调用;
  3. 拥有高权限工具——文件读写、命令执行、数据库查询,正是 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 的答案很清醒:辅助函数容易被忽略,而运行时探测(用 http.LocalAddrContextKey)是最向后兼容、也最不容易被意外禁用的方案。

3.2 核心机制:LocalAddr + Host 双重校验

文字版逻辑链:

请求进入 StreamableHTTPHandler.ServeHTTP


[1] 读 http.LocalAddrContextKey —— 这次 TCP 连接的本地地址是什么?

        ├── 非 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。

修复后 isLocalhostAddrisLocalhostHost 两个辅助函数承担判断职责。修复行为可以用一段等效逻辑表达:

go
// 等效逻辑示意(基于 PR #760 描述还原,非逐行源码)
func (h *StreamableHTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if !h.opts.DisableLocalhostProtection {
        localAddr, ok := r.Context().Value(http.LocalAddrContextKey).(*net.TCPAddr)
        if ok && isLocalhostAddr(localAddr) && !isLocalhostHost(r.Host) {
            // 连接来自 loopback,但 Host 头指向别的域名:
            // 典型 DNS rebinding 特征,直接拒绝
            http.Error(w, "Forbidden", http.StatusForbidden)
            return
        }
    }
    // ... 正常的 Streamable HTTP 处理
}

func isLocalhostAddr(addr *net.TCPAddr) bool {
    return addr.IP.IsLoopback() // 覆盖 127.0.0.0/8 与 ::1
}

func isLocalhostHost(host string) bool {
    h, _, err := net.SplitHostPort(host)
    if err != nil {
        h = host // Host 头可能不带端口
    }
    ip := net.ParseIP(h)
    return h == "localhost" || (ip != nil && ip.IsLoopback())
}

注意实现层面的两个细节:

  1. LocalAddrContextKey 而不是解析 r.Host 的端口——防护针对"这条连接到底从哪来",即使服务器绑在 0.0.0.0,只要请求是从 loopback 进来的就启用校验;反过来,从局域网进来的请求完全不受影响。
  2. Host 头要兼容带端口与不带端口两种形态net.SplitHostPort 失败时退回原值,net.ParseIP 判 loopback。

3.3 一致性测试:这不是 Go SDK 的独角戏

PR 附带的验证不只跑单测,还过了 MCP 官方 conformance 测试套件的 dns-rebinding-protection 场景:

  • localhost-host-rebinding-rejected:PASS
  • localhost-host-valid-accepted:PASS

配套的另一块拼图是 TypeScript SDK 的同名实现——localhostHostValidation() 中间件。也就是说,DNS rebinding 防护是规范级要求 + 官方 SDK 统一落地,两个主要语言 SDK 行为对齐。写跨语言 MCP 组件时可以预期一致的安全基线。

四、影响面与自查清单

4.1 你受影响吗

同时满足以下条件才有实际风险:

  • 使用官方 Go SDK 的 StreamableHTTPHandlerSSEHandler(即 HTTP/SSE 传输);
  • 版本 < 1.4.0
  • 服务器监听 localhost 且无认证
  • 用户会用浏览器访问任意网页(这一条基本无法避免)。

stdio 传输(StdioTransport)的 MCP 服务器与此无关——没有 HTTP 面,rebinding 无从谈起。

4.2 升级与配置

bash
# 升级到 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 原文给出的正是这两条):

go
// 方案 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 服务器的完整加固清单还包括:

  1. 尽量绑 127.0.0.1 而非 0.0.0.0——0.0.0.0 会把服务暴露给整个局域网,等价于把免认证的 MCP 面开放给同网段所有设备(参考站内微软 UFO Mobile MCP 无认证接管的教训:0.0.0.0 绑定是 CVSS 9.4 漏洞的触发器);
  2. 给 HTTP 传输加一层认证,哪怕是简单的 token 校验;
  3. 工具面最小化:不用的工具别注册,文件系统工具限制根目录;
  4. 服务器审计日志记下每一次 tools/call 的来源。

五、几个值得琢磨的细节

其一,"Claude 辅助 PR" 出现在官方安全修复里。 PR 描述明说这是 Claude 辅助完成、用来测试"实现难度"的 PR。一个安全默认值的落地成本,低到可以用 AI 快速验证——这本身就是 MCP 生态工程节奏的一个信号。当然,最终合并的是人工审查后的版本,conformance 测试通过是硬门槛。

其二,防护为什么默认只对 localhost 生效? PR 讨论过把校验扩大到所有请求的可能性,但外部请求场景千差万别(公网部署、带认证、域前置……),SDK 层武断校验 Host 反而会破坏合法部署。最小惊讶原则赢了:只对"免认证 localhost"这个风险最高的组合动刀。

其三,CVSS 的 AT:P 和 UI:P 不是在给漏洞"打折"。 7.6 分背后是 VC:H/VI:H(机密性、完整性影响都是高)——MCP 工具调用能做的事,就是漏洞的影响。攻击复杂度高只是说"需要用户访问恶意页面",而这对真实攻击者从来不是门槛。

六、参考资料

  1. NVD — CVE-2026-34742:https://nvd.nist.gov/vuln/detail/CVE-2026-34742
  2. 修复 commit(PR #760):https://github.com/modelcontextprotocol/go-sdk/commit/67bd3f2e2b53ce11a16db8d976cdb8ff1e986b6d
  3. Red Hat 安全公告 RHSA-2026:21772:https://access.redhat.com/errata/RHSA-2026:21772
  4. MCP 规范 · Security Best Practices(Local MCP Server Compromise):https://modelcontextprotocol.io/specification/2025-11-25/basic/security_best_practices
  5. 站内相关:Serena MCP DNS rebinding RCE(CVE-2026-49471) · 微软 UFO Mobile MCP 无认证接管(CVE-2026-73296) · AWS 官方 MCP 服务器命令注入(CVE-2026-87911)

上次更新于: