
Анализирует конкретную 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
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)) для другого ответа вышестоящего сервиса.backend/internal/handler/auth_wechat_oauth.go:1149 по-прежнему использует сырой io.ReadAll(resp.Body).Анализ: Проект уже признал необходимость ограничения размера тел ответов вышестоящих сервисов, и используются как специальные, так и общие реализации ограничения. Отмеченный путь 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 жёстко задаёт вышестоящие конечные точки как URL-адреса 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/service/crs_sync_service.go:1201, но отмеченная функция не использует ни одну из них.backend/internal/handler/auth_wechat_oauth.go, зарегистрирован в backend/internal/server/routes/auth.go:73-82, а промежуточное ПО режима бэкенда по-прежнему разрешает пути обратного вызова в backend/internal/server/middleware/backend_mode_guard.go:38-55.Истинная уязвимость
Ключевые доказательства:
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).