
Analyzes a specific CVE in WeChat OAuth handler, identifying unbounded HTTP response reads leading to denial of service, with remediation guidance.
backend/internal/handler/auth_wechat_oauth.go:1149io.ReadAll(resp.Body) to read the full upstream HTTP response body into memory without enforcing any maximum size limit before buffering.backend/internal/handler/auth_wechat_oauth.go:1124The flagged function and over 50 lines of surrounding context were reviewed. The sink is the token exchange helper exchangeWeChatOAuthCode(). This function constructs a GET request to WeChat, sends it using a plain http.Client, and then reads the entire response body with 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)
}
...
}
Evidence:
backend/internal/handler/auth_wechat_oauth.go:1142 creates client := &http.Client{Timeout: 30 * time.Second}.backend/internal/handler/auth_wechat_oauth.go:1149 executes body, err := io.ReadAll(resp.Body).io.LimitReader, no ContentLength check, and no helper function call is present before buffering the response body.Analysis: The only protection here is a time limit (Timeout: 30 * time.Second), not a memory limit. A remote peer can still trigger large memory allocations before EOF is reached. The reachability of this sink from production routes was then traced.
The auth route registration and WeChat OAuth handler were reviewed. The route is registered under the public /auth group, not a JWT-protected group. The callback handler receives user-supplied code and state query parameters before invoking the flagged flow.
// 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)
}
Evidence:
backend/internal/server/routes/auth.go:27-28 places these routes in the public /auth router group.backend/internal/server/routes/auth.go:73 registers auth.GET("/oauth/wechat/start", h.Auth.WeChatOAuthStart).backend/internal/server/routes/auth.go:80 registers auth.GET("/oauth/wechat/callback", h.Auth.WeChatOAuthCallback).backend/internal/handler/auth_wechat_oauth.go:160-162 reads code and state from the HTTP request.backend/internal/handler/auth_wechat_oauth.go:206 calls fetchWeChatOAuthIdentity(c.Request.Context(), cfg, code).backend/internal/handler/auth_wechat_oauth.go:105-147 shows the normal pre-processing step that sets a state cookie and redirects the browser into the OAuth flow.Analysis: This is live production code reachable from a public GET callback. The state cookie provides OAuth-CSRF protection, but it does not limit the size of the HTTP response that arrives downstream after the token exchange. The internal call chain and all production callers of the sink were then followed.
exchangeWeChatOAuthCode()The internal call chain between the handler and helper functions was traced. The main callback reaches the sink through a helper (fetchWeChatOAuthIdentity), while the payment callback reaches the same sink directly.
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
}
Evidence:
backend/internal/handler/auth_wechat_oauth.go:1112-1117 shows fetchWeChatOAuthIdentity() calling exchangeWeChatOAuthCode(), then fetchWeChatUserInfo().backend/internal/handler/auth_wechat_oauth.go:206 is where the main OAuth callback calls fetchWeChatOAuthIdentity().backend/internal/handler/auth_wechat_oauth.go:438 is where the payment OAuth callback calls exchangeWeChatOAuthCode() directly.Analysis: The flagged sink is not dead code. A project-wide caller search resolves two production entry paths into the same helper: the standard WeChat login/bind callback and the WeChat payment callback. Any sanitizers, validators, or framework-level size guards present on this response path were then checked.
The codebase was searched for response body size-limiting patterns, and the shared helper already used elsewhere for bounded upstream reads was reviewed. That helper uses io.LimitReader(..., maxBytes+1) and errors explicitly when the limit is exceeded, but the WeChat OAuth code does not use it.
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))