
يحلل ثغرة CVE محددة في معالج OAuth الخاص بتطبيق WeChat، ويحدد قراءات استجابات HTTP غير المحدودة التي تؤدي إلى رفض الخدمة، مع إرشادات المعالجة.
backend/internal/handler/auth_wechat_oauth.go:1149io.ReadAll(resp.Body) لقراءة كامل جسم استجابة HTTP من الخادم العلوي في الذاكرة دون فرض أي حد أقصى للحجم قبل التخزين المؤقت.backend/internal/handler/auth_wechat_oauth.go:1124تمت مراجعة الدالة المحددة وأكثر من 50 سطراً من السياق المحيط بها. نقطة الالتقاط هي دالة مساعدة لتبادل الرمز exchangeWeChatOAuthCode(). تقوم هذه الدالة بإنشاء طلب GET إلى WeChat، وإرساله باستخدام http.Client بسيط، ثم قراءة كامل جسم الاستجابة باستخدام 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)
}
...
}
الأدلة:
backend/internal/handler/auth_wechat_oauth.go:1142 ينشئ client := &http.Client{Timeout: 30 * time.Second}.backend/internal/handler/auth_wechat_oauth.go:1149 ينفذ body, err := io.ReadAll(resp.Body).io.LimitReader، ولا فحص لـ ContentLength، ولا استدعاء لأي دالة مساعدة قبل تخزين جسم الاستجابة مؤقتاً.التحليل: الحماية الوحيدة هنا هي حد زمني (Timeout: 30 * time.Second)، وليس حداً للذاكرة. يمكن لطرف بعيد أن يتسبب في عمليات تخصيص ذاكرة كبيرة قبل الوصول إلى نهاية الملف (EOF). تم بعد ذلك تتبع إمكانية الوصول إلى نقطة الالتقاط هذه من مسارات الإنتاج.
تمت مراجعة تسجيل مسار المصادقة ومعالج WeChat OAuth. المسار مسجل ضمن المجموعة العامة /auth، وليس ضمن مجموعة محمية بـ JWT. يستقبل معالج الاستدعاء معاملات code و state المقدمة من المستخدم قبل استدعاء التدفق المحدد.
// 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)
}
الأدلة:
backend/internal/server/routes/auth.go:27-28 يضعان هذه المسارات في مجموعة المسارات العامة /auth.backend/internal/server/routes/auth.go:73 يسجل auth.GET("/oauth/wechat/start", h.Auth.WeChatOAuthStart).backend/internal/server/routes/auth.go:80 يسجل auth.GET("/oauth/wechat/callback", h.Auth.WeChatOAuthCallback).backend/internal/handler/auth_wechat_oauth.go:160-162 يقرآن code و state من طلب HTTP.backend/internal/handler/auth_wechat_oauth.go:206 يستدعي fetchWeChatOAuthIdentity(c.Request.Context(), cfg, code).backend/internal/handler/auth_wechat_oauth.go:105-147 تُظهر خطوة المعالجة المسبقة العادية التي تعيّن ملف تعريف ارتباط للحالة وتعيد توجيه المتصفح إلى تدفق OAuth.التحليل: هذا كود إنتاج حي يمكن الوصول إليه من استدعاء GET عام. يوفر ملف تعريف ارتباط الحالة حماية OAuth-CSRF، لكنه لا يحد من حجم استجابة HTTP التي تصل لاحقاً بعد تبادل الرمز. تم بعد ذلك تتبع سلسلة الاستدعاءات الداخلية وجميع مستدعي نقطة الالتقاط في الإنتاج.
exchangeWeChatOAuthCode()تم تتبع سلسلة الاستدعاءات الداخلية بين المعالج والدوال المساعدة. يصل استدعاء المعالج الرئيسي إلى نقطة الالتقاط من خلال دالة مساعدة (fetchWeChatOAuthIdentity)، بينما يصل استدعاء الدفع إلى نفس نقطة الالتقاط مباشرة.
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
}
الأدلة:
backend/internal/handler/auth_wechat_oauth.go:1112-1117 تُظهر fetchWeChatOAuthIdentity() تستدعي exchangeWeChatOAuthCode()، ثم fetchWeChatUserInfo().backend/internal/handler/auth_wechat_oauth.go:206 هو المكان الذي يستدعي فيه استدعاء OAuth الرئيسي fetchWeChatOAuthIdentity().backend/internal/handler/auth_wechat_oauth.go:438 هو المكان الذي يستدعي فيه استدعاء OAuth للدفع exchangeWeChatOAuthCode() مباشرة.التحليل: نقطة الالتقاط المحددة ليست كوداً ميتاً. يكشف البحث عن المستدعين على مستوى المشروع عن مسارين دخول للإنتاج إلى نفس الدالة المساعدة: استدعاء تسجيل الدخول/الربط القياسي لـ WeChat واستدعاء الدفع عبر WeChat. تم بعد ذلك فحص أي أدوات تنظيف أو مدققات أو حراسات على مستوى الإطار تفرض حدوداً على الحجم في هذا المسار.
تم البحث في قاعدة الكود عن أنماط تحديد حجم جسم الاستجابة، وتمت مراجعة الدالة المساعدة المشتركة المستخدمة بالفعل في أماكن أخرى للقراءات العلوية المقيدة. تستخدم تلك الدالة المساعدة io.LimitReader(..., maxBytes+1) وتُصدر خطأً صريحاً عند تجاوز الحد، لكن كود WeChat OAuth لا يستخدمها.
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))