
Analysiert eine spezifische CVE im WeChat-OAuth-Handler, identifiziert unbegrenzte HTTP-Antwortlesevorgänge, die zu einem Denial of Service führen, mit Anleitung zur Behebung.
backend/internal/handler/auth_wechat_oauth.go:1149io.ReadAll(resp.Body), um den vollständigen Upstream-HTTP-Antworttext in den Speicher zu lesen, ohne vor dem Puffern eine maximale Größenbeschränkung durchzusetzen.backend/internal/handler/auth_wechat_oauth.go:1124Die markierte Funktion und mehr als 50 Zeilen umgebenden Kontexts wurden überprüft. Die Senke ist der Token-Austausch-Helfer exchangeWeChatOAuthCode(). Diese Funktion erstellt eine GET-Anfrage an WeChat, sendet sie mit einem einfachen http.Client und liest dann den gesamten Antworttext mit 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)
}
...
}
Belege:
backend/internal/handler/auth_wechat_oauth.go:1142 erstellt client := &http.Client{Timeout: 30 * time.Second}.backend/internal/handler/auth_wechat_oauth.go:1149 führt body, err := io.ReadAll(resp.Body) aus.io.LimitReader, keine ContentLength-Prüfung und kein Aufruf einer Hilfsfunktion vorhanden.Analyse: Der einzige Schutz hier ist eine Zeitbegrenzung (Timeout: 30 * time.Second), keine Speicherbegrenzung. Ein entfernter Peer kann dennoch große Speicherzuweisungen auslösen, bevor EOF erreicht wird. Die Erreichbarkeit dieser Senke über Produktionsrouten wurde anschließend nachverfolgt.
Die Authentifizierungsrouten-Registrierung und der WeChat-OAuth-Handler wurden überprüft. Die Route ist in der öffentlichen Gruppe /auth registriert, nicht in einer JWT-geschützten Gruppe. Der Callback-Handler empfängt die vom Benutzer bereitgestellten Abfrageparameter code und state, bevor er den markierten Ablauf aufruft.
// 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)
}
Belege:
backend/internal/server/routes/auth.go:27-28 platziert diese Routen in der öffentlichen Router-Gruppe /auth.backend/internal/server/routes/auth.go:73 registriert auth.GET("/oauth/wechat/start", h.Auth.WeChatOAuthStart).backend/internal/server/routes/auth.go:80 registriert auth.GET("/oauth/wechat/callback", h.Auth.WeChatOAuthCallback).backend/internal/handler/auth_wechat_oauth.go:160-162 liest code und state aus der HTTP-Anfrage.backend/internal/handler/auth_wechat_oauth.go:206 ruft fetchWeChatOAuthIdentity(c.Request.Context(), cfg, code) auf.backend/internal/handler/auth_wechat_oauth.go:105-147 zeigt den normalen Vorverarbeitungsschritt, der ein State-Cookie setzt und den Browser in den OAuth-Ablauf umleitet.Analyse: Dies ist produktiver Live-Code, der über einen öffentlichen GET-Callback erreichbar ist. Das State-Cookie bietet OAuth-CSRF-Schutz, begrenzt jedoch nicht die Größe der HTTP-Antwort, die nach dem Token-Austausch nachgelagert ankommt. Die interne Aufrufkette und alle Produktionsaufrufer der Senke wurden anschließend verfolgt.
exchangeWeChatOAuthCode()Die interne Aufrufkette zwischen dem Handler und den Hilfsfunktionen wurde nachverfolgt. Der Haupt-Callback erreicht die Senke über einen Helfer (fetchWeChatOAuthIdentity), während der Zahlungs-Callback dieselbe Senke direkt erreicht.
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
}
Belege:
backend/internal/handler/auth_wechat_oauth.go:1112-1117 zeigt, dass fetchWeChatOAuthIdentity() exchangeWeChatOAuthCode() und dann fetchWeChatUserInfo() aufruft.backend/internal/handler/auth_wechat_oauth.go:206 ist die Stelle, an der der Haupt-OAuth-Callback fetchWeChatOAuthIdentity() aufruft.backend/internal/handler/auth_wechat_oauth.go:438 ist die Stelle, an der der Zahlungs-OAuth-Callback exchangeWeChatOAuthCode() direkt aufruft.Analyse: Die markierte Senke ist kein toter Code. Eine projektweite Aufrufersuche ergibt zwei Produktionseinstiegspfade in denselben Helfer: den Standard-WeChat-Login/Bind-Callback und den WeChat-Zahlungs-Callback. Es wurde anschließend geprüft, ob auf diesem Antwortpfad Sanitizer, Validatoren oder Größenbegrenzungen auf Framework-Ebene vorhanden sind.
Der Codebestand wurde nach Mustern zur Begrenzung der Antworttextgröße durchsucht, und der gemeinsam genutzte Helfer, der bereits an anderer Stelle für begrenzte Upstream-Lesevorgänge verwendet wird, wurde überprüft. Dieser Helfer verwendet io.LimitReader(..., maxBytes+1) und meldet explizit einen Fehler, wenn die Begrenzung überschritten wird, aber der WeChat-OAuth-Code verwendet ihn nicht.
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