
Analizza una specifica CVE nel gestore OAuth di WeChat, identificando letture illimitate delle risposte HTTP che portano a denial of service, con indicazioni di rimedio.
backend/internal/handler/auth_wechat_oauth.go:1149io.ReadAll(resp.Body) per leggere l'intero corpo della risposta HTTP upstream in memoria senza imporre alcun limite massimo di dimensione prima del buffering.backend/internal/handler/auth_wechat_oauth.go:1124La funzione segnalata e oltre 50 righe di contesto circostante sono state esaminate. Il sink è l'helper di scambio token exchangeWeChatOAuthCode(). Questa funzione costruisce una richiesta GET verso WeChat, la invia utilizzando un semplice http.Client, e poi legge l'intero corpo della risposta 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)
}
...
}
Evidenza:
backend/internal/handler/auth_wechat_oauth.go:1142 crea client := &http.Client{Timeout: 30 * time.Second}.backend/internal/handler/auth_wechat_oauth.go:1149 esegue body, err := io.ReadAll(resp.Body).io.LimitReader, nessun controllo ContentLength e nessuna chiamata a funzione helper è presente prima del buffering del corpo della risposta.Analisi: L'unica protezione qui è un limite di tempo (Timeout: 30 * time.Second), non un limite di memoria. Un peer remoto può comunque innescare grandi allocazioni di memoria prima che venga raggiunto EOF. È stata poi tracciata la raggiungibilità di questo sink dalle route di produzione.
Sono stati esaminati la registrazione delle route di autenticazione e l'handler OAuth di WeChat. La route è registrata nel gruppo pubblico /auth, non in un gruppo protetto da JWT. L'handler di callback riceve i parametri di query code e state forniti dall'utente prima di invocare il flusso segnalato.
// 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)
}
Evidenza:
backend/internal/server/routes/auth.go:27-28 colloca queste route nel gruppo pubblico di router /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 legge code e state dalla richiesta HTTP.backend/internal/handler/auth_wechat_oauth.go:206 chiama fetchWeChatOAuthIdentity(c.Request.Context(), cfg, code).backend/internal/handler/auth_wechat_oauth.go:105-147 mostra il normale passaggio di pre-elaborazione che imposta un cookie di stato e reindirizza il browser nel flusso OAuth.Analisi: Questo è codice di produzione attivo raggiungibile da un callback GET pubblico. Il cookie di stato fornisce protezione OAuth-CSRF, ma non limita la dimensione della risposta HTTP che arriva a valle dopo lo scambio del token. Sono stati poi seguiti la catena di chiamate interna e tutti i chiamanti di produzione del sink.
exchangeWeChatOAuthCode()È stata tracciata la catena di chiamate interna tra l'handler e le funzioni helper. Il callback principale raggiunge il sink tramite un helper (fetchWeChatOAuthIdentity), mentre il callback di pagamento raggiunge lo stesso sink direttamente.
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
}
Evidenza:
backend/internal/handler/auth_wechat_oauth.go:1112-1117 mostra fetchWeChatOAuthIdentity() che chiama exchangeWeChatOAuthCode(), poi fetchWeChatUserInfo().backend/internal/handler/auth_wechat_oauth.go:206 è il punto in cui il callback OAuth principale chiama fetchWeChatOAuthIdentity().backend/internal/handler/auth_wechat_oauth.go:438 è il punto in cui il callback OAuth di pagamento chiama exchangeWeChatOAuthCode() direttamente.Analisi: Il sink segnalato non è codice morto. Una ricerca dei chiamanti a livello di progetto risolve due percorsi di ingresso di produzione nello stesso helper: il callback standard di login/collegamento WeChat e il callback di pagamento WeChat. Sono stati poi verificati eventuali sanitizer, validatori o guardie di dimensione a livello di framework presenti su questo percorso di risposta.
Il codebase è stato cercato per pattern di limitazione della dimensione del corpo della risposta, ed è stato esaminato l'helper condiviso già utilizzato altrove per letture upstream limitate. Tale helper utilizza io.LimitReader(..., maxBytes+1) e genera un errore esplicitamente quando il limite viene superato, ma il codice OAuth di WeChat non lo utilizza.
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