
يحلل ثغرة 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))
الأدلة:
backend/internal/service/upstream_response_limit.go:26-40 تعرّف readUpstreamResponseBodyLimited() باستخدام io.LimitReader(reader, maxBytes+1).backend/internal/service/upstream_response_limit.go:49-61 تعرّف ReadUpstreamResponseBody() كغلاف قراءة مقيد مشترك.backend/internal/config/config.go:55-58 تعرّف DefaultUpstreamResponseReadMaxBytes.backend/internal/service/crs_sync_service.go:1166 يستخدم io.ReadAll(io.LimitReader(resp.Body, 1<<20)) لقراءة استجابة علوية.backend/internal/service/crs_sync_service.go:1201 يستخدم io.ReadAll(io.LimitReader(resp.Body, 5<<20)) لاستجابة علوية أخرى.التحليل: أدرك المشروع بالفعل الحاجة إلى تحديد أحجام أجسام الاستجابات العلوية، ويتم استخدام كل من التطبيقات المخصصة والمشتركة للحدود. يتجاوز مسار WeChat OAuth المحدد هذه الحمايات تماماً. تم بعد ذلك فحص نقطة النهاية البعيدة لتحديد ما إذا كانت ثابتة وما إذا كان ذلك يغير الاستنتاج.
تمت مراجعة تعريفات الثوابت وكود جلب معلومات المستخدم المجاور. نقطة نهاية رمز OAuth مثبتة على WeChat وليست مقدمة من المستخدم، لكن محتوى جسم الاستجابة لا يزال بيانات شبكة بعيدة وتُقرأ دون أي حد للحجم. توجد قراءة علوية ثانية غير مقيدة في نفس الملف لطلب معلومات المستخدم.
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
}
}
الأدلة:
backend/internal/handler/auth_wechat_oauth.go:52-54 تثبت نقاط النهاية العلوية على عناوين WeChat.backend/internal/handler/auth_wechat_oauth.go:1197 ينفذ قراءة ثانية غير مقيدة io.ReadAll(resp.Body) داخل fetchWeChatUserInfo().backend/internal/server/middleware/backend_mode_guard.go:38-55 تسمح صراحةً بـ /auth/oauth/wechat/callback و /auth/oauth/wechat/payment/callback حتى عند تفعيل وضع الخلفية.التحليل: اسم مضيف WeChat الثابت يقلل من سيطرة المهاجم على اختيار نقطة النهاية، لكنه لا يضيف أي حد لحجم الاستجابة. تظل الثغرة قائمة لأن التطبيق يخزن بايتات استجابة بعيدة عشوائية في الذاكرة دون حد أعلى. هذا كود معالج إنتاج، وليس كود اختبار أو عرض أو كود ميت.
exchangeWeChatOAuthCode() يُصدر طلب HTTP خارجي ثم يستدعي io.ReadAll(resp.Body) في السطر backend/internal/handler/auth_wechat_oauth.go:1142-1149 دون أي حارس للحجم.backend/internal/server/routes/auth.go:73 و backend/internal/server/routes/auth.go:80 يسجلان مساري بدء/استدعاء WeChat العامين، والسطر backend/internal/handler/auth_wechat_oauth.go:206 يوجه تدفق الاستدعاء عبر fetchWeChatOAuthIdentity().backend/internal/service/upstream_response_limit.go:26-40 وتستخدم خدمات أخرى io.LimitReader في backend/internal/service/crs_sync_service.go:1166 و ، لكن الدالة المحددة لا تستخدم أياً منها.ثغرة حقيقية
الأدلة الرئيسية:
backend/internal/handler/auth_wechat_oauth.go:1149 يستخدم body, err := io.ReadAll(resp.Body) لقراءة كامل استجابة الرمز العلوية بعد تعيين مهلة فقط — دون حد أقصى للحجم.backend/internal/server/routes/auth.go:73-82 تعرض /oauth/wechat/callback و /oauth/wechat/payment/callback، وكلا المسارين يصلان إلى الدالة المساعدة المحددة عبر backend/internal/handler/auth_wechat_oauth.go:206 و backend/internal/handler/auth_wechat_oauth.go:438.backend/internal/service/upstream_response_limit.go:26-40 و backend/internal/service/crs_sync_service.go:1166 تُظهر أن قاعدة الكود تستخدم بالفعل io.LimitReader للقراءات العلوية المقيدة — كود WeChat OAuth ببساطة لا يفعل ذلك.resp.Body باستخدام io.LimitReader(..., maxBytes+1) ورفض الأجسام التي تتجاوز الحد.backend/internal/handler/auth_wechat_oauth.go:1197، الذي يحتوي على نفس النمط غير المقيد.backend/internal/service/upstream_response_limit.go:26-40 بحيث يكون تدفق OAuth متسقاً مع كيفية تعامل بقية قاعدة الكود مع الاستجابات العلوية.backend/internal/handler/auth_wechat_oauth.go:206) واستدعاء الدفع (backend/internal/handler/auth_wechat_oauth.go:438).backend/internal/handler/auth_wechat_oauth.go:1149 لا يزال يستخدم io.ReadAll(resp.Body) خاماً.backend/internal/service/crs_sync_service.go:1201backend/internal/handler/auth_wechat_oauth.go، وهو مسجل في backend/internal/server/routes/auth.go:73-82، ولا يزال وسيط وضع الخلفية يسمح بمسارات الاستدعاء في backend/internal/server/middleware/backend_mode_guard.go:38-55.