
Analiza un CVE específico en el manejador OAuth de WeChat, identificando lecturas de respuestas HTTP sin límite que provocan denegación de servicio, con orientación de remediación.
backend/internal/handler/auth_wechat_oauth.go:1149io.ReadAll(resp.Body) para leer el cuerpo completo de la respuesta HTTP del proveedor en memoria sin imponer ningún límite máximo de tamaño antes del almacenamiento en búfer.backend/internal/handler/auth_wechat_oauth.go:1124Se revisaron la función marcada y más de 50 líneas de contexto circundante. El punto de origen es la función auxiliar de intercambio de tokens exchangeWeChatOAuthCode(). Esta función construye una solicitud GET a WeChat, la envía usando un http.Client simple y luego lee el cuerpo completo de la respuesta con 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)
}
...
}
Evidencia:
backend/internal/handler/auth_wechat_oauth.go:1142 crea client := &http.Client{Timeout: 30 * time.Second}.backend/internal/handler/auth_wechat_oauth.go:1149 ejecuta body, err := io.ReadAll(resp.Body).io.LimitReader, ni verificación de ContentLength, ni llamada a una función auxiliar antes de almacenar en búfer el cuerpo de la respuesta.Análisis: La única protección aquí es un límite de tiempo (Timeout: 30 * time.Second), no un límite de memoria. Un par remoto aún puede provocar grandes asignaciones de memoria antes de que se alcance EOF. Luego se rastreó la accesibilidad de este punto de origen desde las rutas de producción.
Se revisaron el registro de rutas de autenticación y el manejador OAuth de WeChat. La ruta está registrada bajo el grupo público /auth, no bajo un grupo protegido por JWT. El manejador de devolución de llamada recibe los parámetros de consulta code y state proporcionados por el usuario antes de invocar el flujo marcado.
// 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)
}
Evidencia:
backend/internal/server/routes/auth.go:27-28 coloca estas rutas en el grupo público de enrutadores /auth.backend/internal/server/routes/auth.go:73 registra auth.GET("/oauth/wechat/start", h.Auth.WeChatOAuthStart).backend/internal/server/routes/auth.go:80 registra auth.GET("/oauth/wechat/callback", h.Auth.WeChatOAuthCallback).backend/internal/handler/auth_wechat_oauth.go:160-162 lee code y state de la solicitud HTTP.backend/internal/handler/auth_wechat_oauth.go:206 llama a fetchWeChatOAuthIdentity(c.Request.Context(), cfg, code).backend/internal/handler/auth_wechat_oauth.go:105-147 muestra el paso de preprocesamiento normal que establece una cookie de estado y redirige el navegador al flujo OAuth.Análisis: Este es código de producción activo accesible desde una devolución de llamada GET pública. La cookie de estado proporciona protección OAuth-CSRF, pero no limita el tamaño de la respuesta HTTP que llega posteriormente después del intercambio de tokens. Luego se siguieron la cadena de llamadas interna y todos los llamadores de producción del punto de origen.
exchangeWeChatOAuthCode()Se rastreó la cadena de llamadas interna entre el manejador y las funciones auxiliares. La devolución de llamada principal llega al punto de origen a través de una función auxiliar (fetchWeChatOAuthIdentity), mientras que la devolución de llamada de pago llega al mismo punto de origen directamente.
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
}
Evidencia:
backend/internal/handler/auth_wechat_oauth.go:1112-1117 muestra fetchWeChatOAuthIdentity() llamando a exchangeWeChatOAuthCode() y luego a fetchWeChatUserInfo().backend/internal/handler/auth_wechat_oauth.go:206 es donde la devolución de llamada OAuth principal llama a fetchWeChatOAuthIdentity().backend/internal/handler/auth_wechat_oauth.go:438 es donde la devolución de llamada OAuth de pago llama a exchangeWeChatOAuthCode() directamente.Análisis: El punto de origen marcado no es código muerto. Una búsqueda de llamadores en todo el proyecto revela dos rutas de entrada de producción hacia la misma función auxiliar: la devolución de llamada estándar de inicio de sesión/vinculación de WeChat y la devolución de llamada de pago de WeChat. Luego se verificaron los sanitizadores, validadores o protecciones de tamaño a nivel de framework presentes en esta ruta de respuesta.
Se buscaron en el código base patrones de limitación de tamaño del cuerpo de respuesta y se revisó la función auxiliar compartida ya utilizada en otros lugares para lecturas limitadas de proveedores. Esa función auxiliar utiliza io.LimitReader(..., maxBytes+1) y genera un error explícito cuando se excede el límite, pero el código OAuth de WeChat no la utiliza.
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
}