
Анализирует конкретную 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 показывает обычный этап предварительной обработки, который устанавливает cookie состояния и перенаправляет браузер в поток OAuth.Анализ: Это рабочий производственный код, доступный через публичный GET-обратный вызов. Cookie состояния обеспечивает защиту от 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