
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
raw, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
...
raw, _ := io.ReadAll(io.LimitReader(resp.Body, 5<<20))
Belege:
backend/internal/service/upstream_response_limit.go:26-40 definiert readUpstreamResponseBodyLimited() mit io.LimitReader(reader, maxBytes+1).backend/internal/service/upstream_response_limit.go:49-61 definiert ReadUpstreamResponseBody() als gemeinsamen Wrapper für begrenzte Lesevorgänge.backend/internal/config/config.go:55-58 definiert DefaultUpstreamResponseReadMaxBytes.backend/internal/service/crs_sync_service.go:1166 verwendet io.ReadAll(io.LimitReader(resp.Body, 1<<20)), um eine Upstream-Antwort zu lesen.backend/internal/service/crs_sync_service.go:1201 verwendet io.ReadAll(io.LimitReader(resp.Body, 5<<20)) für eine andere Upstream-Antwort.backend/internal/handler/auth_wechat_oauth.go:1149 weiterhin ein rohes io.ReadAll(resp.Body).Analyse: Das Projekt hat bereits erkannt, dass die Größe von Upstream-Antworttexten begrenzt werden muss, und sowohl Ad-hoc- als auch gemeinsam genutzte Begrenzungsimplementierungen sind im Einsatz. Der markierte WeChat-OAuth-Pfad umgeht diese Schutzmaßnahmen vollständig. Der entfernte Endpunkt wurde anschließend geprüft, um festzustellen, ob er fest ist und ob dies die Schlussfolgerung ändert.
Die Konstantendefinitionen und der angrenzende Code zum Abrufen von Benutzerinformationen wurden überprüft. Der OAuth-Token-Endpunkt ist fest auf WeChat codiert und nicht benutzerbereitgestellt, aber der Inhalt des Antworttexts sind dennoch entfernte Netzwerkdaten, die ohne Größenbegrenzung gelesen werden. Ein zweites unbegrenztes Upstream-Lesen existiert in derselben Datei für die Benutzerinformationsanfrage.
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
}
}
Belege:
backend/internal/handler/auth_wechat_oauth.go:52-54 codiert Upstream-Endpunkte fest auf WeChat-URLs.backend/internal/handler/auth_wechat_oauth.go:1197 führt ein zweites unbegrenztes io.ReadAll(resp.Body) innerhalb von fetchWeChatUserInfo() aus.backend/internal/server/middleware/backend_mode_guard.go:38-55 erlaubt /auth/oauth/wechat/callback und /auth/oauth/wechat/payment/callback ausdrücklich, auch wenn der Backend-Modus aktiviert ist.Analyse: Der feste WeChat-Hostname reduziert die Kontrolle eines Angreifers über die Endpunktauswahl, fügt jedoch keine Begrenzung der Antwortgröße hinzu. Die Schwachstelle bleibt bestehen, da die Anwendung beliebige entfernte Antwortbytes ohne Obergrenze in den Speicher puffert. Dies ist Produktions-Handler-Code, kein Test-, Demo- oder toter Code.
exchangeWeChatOAuthCode() sendet eine ausgehende HTTP-Anfrage und ruft dann io.ReadAll(resp.Body) bei backend/internal/handler/auth_wechat_oauth.go:1142-1149 ohne Größenbegrenzung auf.backend/internal/server/routes/auth.go:73 und backend/internal/server/routes/auth.go:80 registrieren die öffentlichen WeChat-Start-/Callback-Routen, und backend/internal/handler/auth_wechat_oauth.go:206 leitet den Callback-Ablauf durch fetchWeChatOAuthIdentity().backend/internal/service/upstream_response_limit.go:26-40, und andere Dienste verwenden io.LimitReader bei backend/internal/service/crs_sync_service.go:1166 und backend/internal/service/crs_sync_service.go:1201, aber die markierte Funktion verwendet keinen davon.backend/internal/handler/auth_wechat_oauth.go, ist bei backend/internal/server/routes/auth.go:73-82 registriert, und die Backend-Modus-Middleware erlaubt die Callback-Pfade weiterhin bei backend/internal/server/middleware/backend_mode_guard.go:38-55.Echte Schwachstelle
Wichtigste Belege:
backend/internal/handler/auth_wechat_oauth.go:1149 verwendet body, err := io.ReadAll(resp.Body), um die vollständige Upstream-Token-Antwort zu lesen, nachdem nur ein Timeout festgelegt wurde — keine Größenbegrenzung.backend/internal/server/routes/auth.go:73-82 legt /oauth/wechat/callback und /oauth/wechat/payment/callback offen, und beide Routen erreichen den markierten Helfer über backend/internal/handler/auth_wechat_oauth.go:206 und backend/internal/handler/auth_wechat_oauth.go:438.backend/internal/service/upstream_response_limit.go:26-40 und backend/internal/service/crs_sync_service.go:1166 zeigen, dass der Codebestand bereits io.LimitReader für begrenzte Upstream-Lesevorgänge verwendet — der WeChat-OAuth-Code tut dies schlicht nicht.resp.Body mit io.LimitReader(..., maxBytes+1) und Ablehnen von Texten, die die Begrenzung überschreiten.backend/internal/handler/auth_wechat_oauth.go:1197 an, das dasselbe unbegrenzte Muster aufweist.backend/internal/service/upstream_response_limit.go:26-40, damit der OAuth-Ablauf konsistent mit der Art und Weise ist, wie der Rest des Codebestands Upstream-Antworten behandelt.backend/internal/handler/auth_wechat_oauth.go:206) und den Zahlungs-Callback (backend/internal/handler/auth_wechat_oauth.go:438).