
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
raw, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
...
raw, _ := io.ReadAll(io.LimitReader(resp.Body, 5<<20))
Evidenza:
backend/internal/service/upstream_response_limit.go:26-40 definisce readUpstreamResponseBodyLimited() utilizzando io.LimitReader(reader, maxBytes+1).backend/internal/service/upstream_response_limit.go:49-61 definisce ReadUpstreamResponseBody() come wrapper condiviso di lettura limitata.backend/internal/config/config.go:55-58 definisce DefaultUpstreamResponseReadMaxBytes.backend/internal/service/crs_sync_service.go:1166 utilizza io.ReadAll(io.LimitReader(resp.Body, 1<<20)) per leggere una risposta upstream.backend/internal/service/crs_sync_service.go:1201 utilizza io.ReadAll(io.LimitReader(resp.Body, 5<<20)) per un'altra risposta upstream.backend/internal/handler/auth_wechat_oauth.go:1149 utilizza ancora un io.ReadAll(resp.Body) grezzo.Analisi: Il progetto ha già riconosciuto la necessità di limitare le dimensioni dei corpi delle risposte upstream, e sia le implementazioni di limite ad hoc che quelle condivise sono in uso. Il percorso OAuth di WeChat segnalato bypassa completamente queste protezioni. È stato poi verificato l'endpoint remoto per determinare se è fisso e se ciò cambia la conclusione.
Sono state esaminate le definizioni delle costanti e il codice adiacente di recupero informazioni utente. L'endpoint del token OAuth è hardcoded su WeChat e non è fornito dall'utente, ma il contenuto del corpo della risposta è comunque dati di rete remoti e viene letto senza alcun limite di dimensione. Una seconda lettura upstream illimitata esiste nello stesso file per la richiesta di informazioni utente.
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
}
}
Evidenza:
backend/internal/handler/auth_wechat_oauth.go:52-54 hardcoda gli endpoint upstream agli URL di WeChat.backend/internal/handler/auth_wechat_oauth.go:1197 esegue un secondo io.ReadAll(resp.Body) illimitato all'interno di fetchWeChatUserInfo().backend/internal/server/middleware/backend_mode_guard.go:38-55 consente esplicitamente /auth/oauth/wechat/callback e /auth/oauth/wechat/payment/callback anche quando la modalità backend è abilitata.Analisi: Il nome host WeChat fisso riduce il controllo di un attaccante sulla selezione dell'endpoint, ma non aggiunge alcun limite di dimensione della risposta. La debolezza rimane perché l'applicazione bufferizza byte arbitrari di risposta remota in memoria senza un limite superiore. Questo è codice di handler di produzione, non codice di test, demo o morto.
exchangeWeChatOAuthCode() emette una richiesta HTTP in uscita e poi chiama io.ReadAll(resp.Body) in backend/internal/handler/auth_wechat_oauth.go:1142-1149 senza alcuna guardia di dimensione.backend/internal/server/routes/auth.go:73 e backend/internal/server/routes/auth.go:80 registrano le route pubbliche di start/callback WeChat, e backend/internal/handler/auth_wechat_oauth.go:206 instrada il flusso di callback attraverso fetchWeChatOAuthIdentity().backend/internal/service/upstream_response_limit.go:26-40 e altri servizi utilizzano io.LimitReader in backend/internal/service/crs_sync_service.go:1166 e backend/internal/service/crs_sync_service.go:1201, ma la funzione segnalata non ne utilizza nessuno.backend/internal/handler/auth_wechat_oauth.go, è registrato in backend/internal/server/routes/auth.go:73-82, e il middleware di modalità backend consente ancora i percorsi di callback in backend/internal/server/middleware/backend_mode_guard.go:38-55.Vulnerabilità Vera
Evidenza chiave:
backend/internal/handler/auth_wechat_oauth.go:1149 utilizza body, err := io.ReadAll(resp.Body) per leggere l'intera risposta del token upstream dopo aver impostato solo un timeout — nessun limite di dimensione.backend/internal/server/routes/auth.go:73-82 espone /oauth/wechat/callback e /oauth/wechat/payment/callback, ed entrambe le route raggiungono l'helper segnalato tramite backend/internal/handler/auth_wechat_oauth.go:206 e backend/internal/handler/auth_wechat_oauth.go:438.backend/internal/service/upstream_response_limit.go:26-40 e backend/internal/service/crs_sync_service.go:1166 mostrano che il codebase utilizza già io.LimitReader per letture upstream limitate — il codice OAuth di WeChat semplicemente non lo fa.resp.Body con io.LimitReader(..., maxBytes+1) e rifiutare i corpi che superano il limite.backend/internal/handler/auth_wechat_oauth.go:1197, che presenta lo stesso identico pattern illimitato.backend/internal/service/upstream_response_limit.go:26-40 affinché il flusso OAuth sia coerente con il modo in cui il resto del codebase gestisce le risposte upstream.backend/internal/handler/auth_wechat_oauth.go:206) e il callback di pagamento (backend/internal/handler/auth_wechat_oauth.go:438).