
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
}
// 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))
Evidencia:
backend/internal/service/upstream_response_limit.go:26-40 define readUpstreamResponseBodyLimited() usando io.LimitReader(reader, maxBytes+1).backend/internal/service/upstream_response_limit.go:49-61 define ReadUpstreamResponseBody() como un envoltorio compartido de lectura limitada.backend/internal/config/config.go:55-58 define DefaultUpstreamResponseReadMaxBytes.backend/internal/service/crs_sync_service.go:1166 usa io.ReadAll(io.LimitReader(resp.Body, 1<<20)) para leer una respuesta de un proveedor.backend/internal/service/crs_sync_service.go:1201 usa io.ReadAll(io.LimitReader(resp.Body, 5<<20)) para otra respuesta de un proveedor.backend/internal/handler/auth_wechat_oauth.go:1149 todavía usa un io.ReadAll(resp.Body) sin procesar.Análisis: El proyecto ya ha reconocido la necesidad de limitar los tamaños de los cuerpos de respuesta de los proveedores, y tanto las implementaciones de límite ad-hoc como las compartidas están en uso. La ruta OAuth de WeChat marcada evita estas protecciones por completo. Luego se verificó el endpoint remoto para determinar si es fijo y si eso cambia la conclusión.
Se revisaron las definiciones de constantes y el código adyacente de obtención de información del usuario. El endpoint de token OAuth está codificado a WeChat y no es proporcionado por el usuario, pero el contenido del cuerpo de la respuesta sigue siendo datos de red remotos y se lee sin ningún límite de tamaño. Existe una segunda lectura no limitada de un proveedor en el mismo archivo para la solicitud de información del usuario.
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
}
}
Evidencia:
backend/internal/handler/auth_wechat_oauth.go:52-54 codifica los endpoints de proveedores a URLs de WeChat.backend/internal/handler/auth_wechat_oauth.go:1197 realiza una segunda lectura no limitada io.ReadAll(resp.Body) dentro de fetchWeChatUserInfo().backend/internal/server/middleware/backend_mode_guard.go:38-55 permite explícitamente /auth/oauth/wechat/callback y /auth/oauth/wechat/payment/callback incluso cuando el modo backend está habilitado.Análisis: El nombre de host fijo de WeChat reduce el control de un atacante sobre la selección del endpoint, pero no agrega ningún límite de tamaño de respuesta. La debilidad persiste porque la aplicación almacena en búfer bytes arbitrarios de respuestas remotas en memoria sin un límite superior. Este es código de manejador de producción, no código de prueba, demostración o código muerto.
exchangeWeChatOAuthCode() emite una solicitud HTTP saliente y luego llama a io.ReadAll(resp.Body) en backend/internal/handler/auth_wechat_oauth.go:1142-1149 sin protección de tamaño.backend/internal/server/routes/auth.go:73 y backend/internal/server/routes/auth.go:80 registran las rutas públicas de inicio/devolución de llamada de WeChat, y backend/internal/handler/auth_wechat_oauth.go:206 enruta el flujo de devolución de llamada a través de fetchWeChatOAuthIdentity().backend/internal/service/upstream_response_limit.go:26-40 y otros servicios usan io.LimitReader en backend/internal/service/crs_sync_service.go:1166 y backend/internal/service/crs_sync_service.go:1201, pero la función marcada no usa ninguna de ellas.backend/internal/handler/auth_wechat_oauth.go, está registrado en backend/internal/server/routes/auth.go:73-82, y el middleware de modo backend aún permite las rutas de devolución de llamada en backend/internal/server/middleware/backend_mode_guard.go:38-55.Vulnerabilidad real
Evidencia clave:
backend/internal/handler/auth_wechat_oauth.go:1149 usa body, err := io.ReadAll(resp.Body) para leer la respuesta completa del token del proveedor después de establecer solo un tiempo de espera — sin límite de tamaño.backend/internal/server/routes/auth.go:73-82 expone /oauth/wechat/callback y /oauth/wechat/payment/callback, y ambas rutas llegan a la función auxiliar marcada a través de backend/internal/handler/auth_wechat_oauth.go:206 y backend/internal/handler/auth_wechat_oauth.go:438.backend/internal/service/upstream_response_limit.go:26-40 y backend/internal/service/crs_sync_service.go:1166 muestran que el código base ya usa io.LimitReader para lecturas limitadas de proveedores — el código OAuth de WeChat simplemente no lo hace.resp.Body con io.LimitReader(..., maxBytes+1) y rechace los cuerpos que excedan el límite.backend/internal/handler/auth_wechat_oauth.go:1197, que tiene el mismo patrón no limitado.backend/internal/service/upstream_response_limit.go:26-40 para que el flujo OAuth sea consistente con cómo el resto del código base maneja las respuestas de proveedores.backend/internal/handler/auth_wechat_oauth.go:206) y la devolución de llamada de pago (backend/internal/handler/auth_wechat_oauth.go:438).