
Analyse une CVE spécifique dans le gestionnaire OAuth de WeChat, identifiant des lectures de réponses HTTP non bornées menant à un déni de service, avec des conseils de remédiation.
backend/internal/handler/auth_wechat_oauth.go:1149io.ReadAll(resp.Body) pour lire l'intégralité du corps de la réponse HTTP en amont en mémoire sans imposer de limite de taille maximale avant la mise en mémoire tampon.backend/internal/handler/auth_wechat_oauth.go:1124La fonction signalée et plus de 50 lignes de contexte environnant ont été examinées. Le point sensible est la fonction d'aide à l'échange de jeton exchangeWeChatOAuthCode(). Cette fonction construit une requête GET vers WeChat, l'envoie à l'aide d'un http.Client simple, puis lit l'intégralité du corps de la réponse avec 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)
}
...
}
Preuves :
backend/internal/handler/auth_wechat_oauth.go:1142 crée client := &http.Client{Timeout: 30 * time.Second}.backend/internal/handler/auth_wechat_oauth.go:1149 exécute body, err := io.ReadAll(resp.Body).io.LimitReader, aucune vérification de ContentLength et aucun appel de fonction d'aide n'est présent avant la mise en mémoire tampon du corps de la réponse.Analyse : La seule protection ici est une limite de temps (Timeout: 30 * time.Second), pas une limite de mémoire. Un pair distant peut toujours déclencher d'importantes allocations mémoire avant que EOF ne soit atteint. L'accessibilité de ce point sensible depuis les routes de production a ensuite été retracée.
L'enregistrement des routes d'authentification et le gestionnaire OAuth WeChat ont été examinés. La route est enregistrée sous le groupe public /auth, et non sous un groupe protégé par JWT. Le gestionnaire de rappel reçoit les paramètres de requête code et state fournis par l'utilisateur avant d'invoquer le flux signalé.
// 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)
}
Preuves :
backend/internal/server/routes/auth.go:27-28 place ces routes dans le groupe de routeurs public /auth.backend/internal/server/routes/auth.go:73 enregistre auth.GET("/oauth/wechat/start", h.Auth.WeChatOAuthStart).backend/internal/server/routes/auth.go:80 enregistre auth.GET("/oauth/wechat/callback", h.Auth.WeChatOAuthCallback).backend/internal/handler/auth_wechat_oauth.go:160-162 lit code et state depuis la requête HTTP.backend/internal/handler/auth_wechat_oauth.go:206 appelle fetchWeChatOAuthIdentity(c.Request.Context(), cfg, code).backend/internal/handler/auth_wechat_oauth.go:105-147 montre l'étape de pré-traitement normale qui définit un cookie d'état et redirige le navigateur dans le flux OAuth.Analyse : Il s'agit de code de production actif accessible depuis un rappel GET public. Le cookie d'état fournit une protection OAuth-CSRF, mais il ne limite pas la taille de la réponse HTTP qui arrive en aval après l'échange de jeton. La chaîne d'appels interne et tous les appelants de production du point sensible ont ensuite été suivis.
exchangeWeChatOAuthCode()La chaîne d'appels interne entre le gestionnaire et les fonctions d'aide a été retracée. Le rappel principal atteint le point sensible via une fonction d'aide (fetchWeChatOAuthIdentity), tandis que le rappel de paiement atteint le même point sensible directement.
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
}
Preuves :
backend/internal/handler/auth_wechat_oauth.go:1112-1117 montre fetchWeChatOAuthIdentity() appelant exchangeWeChatOAuthCode(), puis fetchWeChatUserInfo().backend/internal/handler/auth_wechat_oauth.go:206 est l'endroit où le rappel OAuth principal appelle fetchWeChatOAuthIdentity().backend/internal/handler/auth_wechat_oauth.go:438 est l'endroit où le rappel OAuth de paiement appelle exchangeWeChatOAuthCode() directement.Analyse : Le point sensible signalé n'est pas du code mort. Une recherche d'appelants à l'échelle du projet révèle deux chemins d'entrée de production vers la même fonction d'aide : le rappel standard de connexion/liaison WeChat et le rappel de paiement WeChat. Les éventuels sanitiseurs, validateurs ou gardes de taille au niveau du framework présents sur ce chemin de réponse ont ensuite été vérifiés.
La base de code a été recherchée pour les modèles de limitation de taille du corps de réponse, et la fonction d'aide partagée déjà utilisée ailleurs pour les lectures en amont bornées a été examinée. Cette fonction d'aide utilise io.LimitReader(..., maxBytes+1) et génère une erreur explicite lorsque la limite est dépassée, mais le code OAuth WeChat ne l'utilise pas.
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
}