编程 Go 升级笔记:ReverseProxy.Director 废弃与 GOPROXY/GOSUMDB 校验绕过 CVE

2026-08-30 21:03:53

上个月把网关从 Go 1.24 升到 1.26.0,IDE 和 staticcheck 开始标出 httputil.ReverseProxy.Director 已废弃。用 go docnet/http/httputil 包,能看到 Director 字段的 Deprecated 标记,官方给的迁移方向是 Go 1.20 引入的 Rewrite 钩子。这不止是换函数签名的问题:Director 拿到的 req 来自客户端原始请求,反向代理在转发给后端前会做 hop-by-hop 头清理。恶意客户端可以在请求里声明 Connection: X-Auth-User,代理清理时会把 Director 刚注入的 X-Auth-User 当成 hop-by-hop 头一起剥掉。后端拿到的请求丢了认证身份,可能被当成匿名请求处理。

Hop-by-hop 头完整列表是:

Connection
Keep-Alive
Proxy-Authenticate
Proxy-Authorization
TE
Trailer
Transfer-Encoding
Upgrade
以及 Connection 头里列出的任何头

复现步骤很简单。起一个后端打印收到的请求头,Director 里注入 X-Auth-User: svc-backend,客户端请求带 Connection: X-Auth-User

proxy := &httputil.ReverseProxy{
    Director: func(req *http.Request) {
        req.Header.Set("X-Auth-User", "svc-backend")
    },
}
curl -H 'Connection: X-Auth-User' http://127.0.0.1:8080/api

后端日志里 X-Auth-User 是空的。Director 废弃不是形式问题,是实际安全漏洞。

迁移到 Rewrite 时,改头对象不同。Rewrite 收到的是 *httputil.ProxyRequestIn 是原始请求,Out 是新构造的请求副本,要往 pr.Out.Header 上设置,不是 pr.In.Header

proxy := &httputil.ReverseProxy{
    Rewrite: func(pr *httputil.ProxyRequest) {
        pr.SetURL(svcBackendURL)
        pr.Out.Header.Set("X-Auth-User", "svc-backend")
    },
}

迁移中踩到的几个坑:

  • Director 里手动设置的 req.URL.Schemereq.URL.Host,在 Rewrite 里可以直接用 pr.SetURL(url) 代替,Host 也会跟着处理。
  • pr.In.Header 里写 Header 不生效,In 只用来读客户端请求语义。
  • 旧 Director 里如果有"决定是否放行、直接写响应"的逻辑,迁到 Rewrite 后要挪到 pr.In.Context() 或请求链路上处理,Rewrite 阶段拿不到 http.ResponseWriter,不能直接写响应。
  • 只把函数签名换成 Rewrite 但不设置转发目标,Out 就是空的,行为直接错乱。

顺带记录另一个坑:Go 1.26.6 和 1.25.13 的安全公告修了十项安全问题,其中两项是恶意的 GOPROXYGOSUMDB 可以绕过校验和数据库,把模块内容偷偷替换成攻击者版本。这直接影响"有 go.sum 就安全"的直觉——如果代理或校验和服务器本身不可信,它可以在首次拉取时给出伪造的校验和,go.sum 存下的是坏 hash,后续 go mod verify 也发现不了差异。

检查命令:

go env GOPROXY GOSUMDB
go mod verify

go mod verify 只验证已下载模块和 go.sum 的一致性,防不住"首次写入就写入坏 hash"的场景。确认 GOSUMDB 指向官方 sumdb,别在不可信环境里随便用自定义 GOPROXY。

版本升级路径按原文结论直接执行:

  • 1.26.0 到 1.26.6 全部升到 1.26.7+
  • 1.25.x 分支需要 1.25.13+(原文未说明 1.25.14 是否存在)
  • 1.26.7 修复的是 net/http 相关 bug

升级时把本机和 CI 的 Go toolchain 换成 1.26.7 即可,go.mod 里的 go 1.26 不用改,patch 版本不影响最小 go 指令。如果项目用了 toolchain 指令,可以用命令触发切换:

go mod edit -toolchain=go1.26.7
go mod verify

最后给迁移加一个回归测试,模拟客户端用 Connection 声明剥除头的攻击方式,防止日后有人把 Director 改回去:

func TestRewriteKeepsAuthHeader(t *testing.T) {
	backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "X-Auth-User=%q", r.Header.Get("X-Auth-User"))
	}))
	defer backend.Close()

	proxy := &httputil.ReverseProxy{
		Rewrite: func(pr *httputil.ProxyRequest) {
			pr.SetURL(mustURL(backend.URL))
			pr.Out.Header.Set("X-Auth-User", "svc-backend")
		},
	}

	req := httptest.NewRequest("GET", "http://gateway/api", nil)
	req.Header.Set("Connection", "X-Auth-User") // 模拟恶意 hop-by-hop 声明
	rr := httptest.NewRecorder()

	proxy.ServeHTTP(rr, req)

	if got := rr.Body.String(); !strings.Contains(got, "X-Auth-User=\"svc-backend\"") {
		t.Fatalf("auth header was stripped: %s", got)
	}
}

func mustURL(raw string) *url.URL {
	u, err := url.Parse(raw)
	if err != nil {
		panic(err)
	}
	return u
}

废弃警告的精确输出文本以实际工具链为准,原文未提供;Director 的移除时间也是原文未提供的信息,1.26 只是正式标记 Deprecated。

复制全文 生成海报 Go ReverseProxy 安全 升级

推荐文章

程序员茄子在线接单