backend/internal/handler/auth_wechat_oauth.go:1149io.ReadAll(resp.Body) 将完整的上游 HTTP 响应体读入内存,在缓冲之前未强制执行任何最大大小限制。backend/internal/handler/auth_wechat_oauth.go:1124 处标记的接收点已审查标记函数及其周围 50 多行上下文。接收点是令牌交换辅助函数 exchangeWeChatOAuthCode()。该函数构造一个对微信的 GET 请求,使用普通 http.Client 发送,然后使用 io.ReadAll(resp.Body) 读取整个响应体。
func exchangeWeChatOAuthCode(ctx context.Context, cfg wechatOAuthConfig, code string) (*wechatOAuthTokenResponse, error) {
endpoint, err := url.Parse(wechatOAuthAccessTokenURL)
...
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)
...
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
...
defer func() { _ = resp.Body.Close() }()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("read wechat access token response: %w", err)
}
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
return nil, fmt.Errorf("wechat access token status=%d", resp.StatusCode)
}
...
}
证据:
backend/internal/handler/auth_wechat_oauth.go:1142 创建 client := &http.Client{Timeout: 30 * time.Second}。backend/internal/handler/auth_wechat_oauth.go:1149 执行 body, err := io.ReadAll(resp.Body)。io.LimitReader、没有 ContentLength 检查,也没有调用辅助函数。分析:这里唯一的保护是时间限制(Timeout: 30 * time.Second),而不是内存限制。远程对端仍然可以在到达 EOF 之前触发大量内存分配。随后追踪了该接收点从生产路由的可达性。
已审查认证路由注册和微信 OAuth 处理器。该路由注册在公共 /auth 组下,而非 JWT 保护的组。回调处理器在调用标记流程之前接收用户提供的 code 和 state 查询参数。
// routes
auth := v1.Group("/auth")
auth.Use(servermiddleware.BackendModeAuthGuard(settingService))
...
auth.GET("/oauth/wechat/start", h.Auth.WeChatOAuthStart)
auth.GET("/oauth/wechat/callback", h.Auth.WeChatOAuthCallback)
// callback handler
func (h *AuthHandler) WeChatOAuthCallback(c *gin.Context) {
...
code := strings.TrimSpace(c.Query("code"))
state := strings.TrimSpace(c.Query("state"))
if code == "" || state == "" {
redirectOAuthError(c, frontendCallback, "missing_params", "missing code/state", "")
return
}
...
tokenResp, userInfo, err := fetchWeChatOAuthIdentity(c.Request.Context(), cfg, code)
if err != nil {
redirectOAuthError(c, frontendCallback, "provider_error", "wechat_identity_fetch_failed", singleLine(err.Error()))
return
}
...
}
// start handler
func (h *AuthHandler) WeChatOAuthStart(c *gin.Context) {
...
state, err := oauth.GenerateState()
...
wechatSetCookie(c, wechatOAuthStateCookieName, encodeCookieValue(state), wechatOAuthCookieMaxAgeSec, secureCookie)
...
c.Redirect(http.StatusFound, authURL)
}
证据:
backend/internal/server/routes/auth.go:27-28 将这些路由放置在公共 /auth 路由器组中。backend/internal/server/routes/auth.go:73 注册 auth.GET("/oauth/wechat/start", h.Auth.WeChatOAuthStart)。backend/internal/server/routes/auth.go:80 注册 auth.GET("/oauth/wechat/callback", h.Auth.WeChatOAuthCallback)。backend/internal/handler/auth_wechat_oauth.go:160-162 从 HTTP 请求中读取 code 和 state。backend/internal/handler/auth_wechat_oauth.go:206 调用 fetchWeChatOAuthIdentity(c.Request.Context(), cfg, code)。backend/internal/handler/auth_wechat_oauth.go:105-147 显示了正常的预处理步骤,该步骤设置状态 cookie 并将浏览器重定向到 OAuth 流程。分析:这是可从公共 GET 回调访问的活跃生产代码。状态 cookie 提供 OAuth-CSRF 保护,但它不限制令牌交换后到达的下游 HTTP 响应的大小。随后追踪了内部调用链和接收点的所有生产调用者。
exchangeWeChatOAuthCode() 的所有生产调用者已追踪处理器与辅助函数之间的内部调用链。主回调通过辅助函数(fetchWeChatOAuthIdentity)到达接收点,而支付回调直接到达同一接收点。
func fetchWeChatOAuthIdentity(ctx context.Context, cfg wechatOAuthConfig, code string) (*wechatOAuthTokenResponse, *wechatOAuthUserInfoResponse, error) {
tokenResp, err := exchangeWeChatOAuthCode(ctx, cfg, code)
if err != nil {
return nil, nil, err
}
userInfo, err := fetchWeChatUserInfo(ctx, tokenResp)
...
}
cfg, err := h.getWeChatOAuthConfig(c.Request.Context(), "mp", c)
...
cfg.redirectURI = h.resolveWeChatPaymentOAuthCallbackURL(c.Request.Context(), c)
tokenResp, err := exchangeWeChatOAuthCode(c.Request.Context(), cfg, code)
if err != nil {
redirectOAuthError(c, frontendCallback, "token_exchange_failed", "failed to exchange oauth code", err.Error())
return
}
证据:
backend/internal/handler/auth_wechat_oauth.go:1112-1117 显示 fetchWeChatOAuthIdentity() 调用 exchangeWeChatOAuthCode(),然后调用 fetchWeChatUserInfo()。backend/internal/handler/auth_wechat_oauth.go:206 是主 OAuth 回调调用 fetchWeChatOAuthIdentity() 的位置。backend/internal/handler/auth_wechat_oauth.go:438 是支付 OAuth 回调直接调用 exchangeWeChatOAuthCode() 的位置。分析:标记的接收点不是死代码。项目范围的调用者搜索解析出两条进入同一辅助函数的生产入口路径:标准微信登录/绑定回调和微信支付回调。随后检查了此响应路径上存在的任何清理器、验证器或框架级大小保护。
搜索了代码库中的响应体大小限制模式,并审查了代码库其他位置已用于有界上游读取的共享辅助函数。该辅助函数使用 io.LimitReader(..., maxBytes+1),并在超过限制时显式报错,但微信 OAuth 代码未使用它。
func readUpstreamResponseBodyLimited(reader io.Reader, maxBytes int64) ([]byte, error) {
...
body, err := io.ReadAll(io.LimitReader(reader, maxBytes+1))
if err != nil {
return nil, err
}
if int64(len(body)) > maxBytes {
return nil, fmt.Errorf("%w: limit=%d", ErrUpstreamResponseBodyTooLarge, maxBytes)
}
return body, nil
}
// DefaultUpstreamResponseReadMaxBytes is the default read cap for upstream non-streaming response bodies.
const DefaultUpstreamResponseReadMaxBytes int64 = 128 * 1024 * 1024
raw, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
...
raw, _ := io.ReadAll(io.LimitReader(resp.Body, 5<<20))
证据:
backend/internal/service/upstream_response_limit.go:26-40 使用 io.LimitReader(reader, maxBytes+1) 定义 readUpstreamResponseBodyLimited()。backend/internal/service/upstream_response_limit.go:49-61 将 ReadUpstreamResponseBody() 定义为共享的有界读取包装器。backend/internal/config/config.go:55-58 定义 DefaultUpstreamResponseReadMaxBytes。backend/internal/service/crs_sync_service.go:1166 使用 io.ReadAll(io.LimitReader(resp.Body, 1<<20)) 读取上游响应。backend/internal/service/crs_sync_service.go:1201 使用 io.ReadAll(io.LimitReader(resp.Body, 5<<20)) 读取另一个上游响应。分析:项目已经认识到限制上游响应体大小的必要性,并且临时和共享的限制实现都在使用中。标记的微信 OAuth 路径完全绕过了这些保护。随后检查了远程端点以确定它是否固定以及这是否改变结论。
已审查常量定义和相邻的用户信息获取代码。OAuth 令牌端点硬编码为微信且不由用户提供,但响应体内容仍然是远程网络数据,并且在没有任何大小限制的情况下被读取。同一文件中存在第二个无界上游读取,用于用户信息请求。
var (
wechatOAuthAccessTokenURL = "https://api.weixin.qq.com/sns/oauth2/access_token"
wechatOAuthUserInfoURL = "https://api.weixin.qq.com/sns/userinfo"
)
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("read wechat userinfo response: %w", err)
}
for _, suffix := range []string{
"/auth/oauth/linuxdo/callback",
"/auth/oauth/wechat/callback",
"/auth/oauth/wechat/payment/callback",
...
} {
if strings.HasSuffix(path, suffix) {
return true
}
}
证据:
backend/internal/handler/auth_wechat_oauth.go:52-54 将上游端点硬编码为微信 URL。backend/internal/handler/auth_wechat_oauth.go:1197 在 fetchWeChatUserInfo() 内执行第二次无界 io.ReadAll(resp.Body)。backend/internal/server/middleware/backend_mode_guard.go:38-55 明确允许 /auth/oauth/wechat/callback 和 /auth/oauth/wechat/payment/callback,即使在启用后端模式时也是如此。分析:固定的微信主机名减少了攻击者对端点选择的控制,但并未添加任何响应大小限制。该弱点仍然存在,因为应用程序在没有上限的情况下将任意远程响应字节缓冲到内存中。这是生产处理器代码,而非测试、演示或死代码。
exchangeWeChatOAuthCode() 发出出站 HTTP 请求,然后在 backend/internal/handler/auth_wechat_oauth.go:1142-1149 处调用 io.ReadAll(resp.Body),且没有大小保护。backend/internal/server/routes/auth.go:73 和 backend/internal/server/routes/auth.go:80 注册了公共微信开始/回调路由,backend/internal/handler/auth_wechat_oauth.go:206 通过 fetchWeChatOAuthIdentity() 路由回调流程。backend/internal/service/upstream_response_limit.go:26-40,其他服务在 backend/internal/service/crs_sync_service.go:1166 和 backend/internal/service/crs_sync_service.go:1201 处使用 io.LimitReader,但标记的函数未使用其中任何一个。真实漏洞
关键证据:
backend/internal/handler/auth_wechat_oauth.go:1149 使用 body, err := io.ReadAll(resp.Body) 在仅设置超时后读取完整的上游令牌响应——没有大小上限。backend/internal/server/routes/auth.go:73-82 暴露 /oauth/wechat/callback 和 /oauth/wechat/payment/callback,两条路由通过 backend/internal/handler/auth_wechat_oauth.go:206 和 backend/internal/handler/auth_wechat_oauth.go:438 到达标记的辅助函数。backend/internal/service/upstream_response_limit.go:26-40 和 backend/internal/service/crs_sync_service.go:1166 表明代码库已使用 io.LimitReader 进行有界上游读取——微信 OAuth 代码只是没有使用。io.LimitReader(..., maxBytes+1) 包装 resp.Body,并拒绝超过限制的响应体。backend/internal/handler/auth_wechat_oauth.go:1197 处相邻的用户信息读取,该处具有相同的无界模式。backend/internal/service/upstream_response_limit.go:26-40 中现有的有界读取模式,使 OAuth 流程与代码库其余部分处理上游响应的方式保持一致。backend/internal/handler/auth_wechat_oauth.go:206)和支付回调(backend/internal/handler/auth_wechat_oauth.go:438)。backend/internal/handler/auth_wechat_oauth.go:1149io.ReadAll(resp.Body)backend/internal/handler/auth_wechat_oauth.go 中,注册于 backend/internal/server/routes/auth.go:73-82,后端模式中间件仍允许 backend/internal/server/middleware/backend_mode_guard.go:38-55 中的回调路径。