
Analisa um CVE específico no manipulador OAuth do WeChat, identificando leituras ilimitadas de respostas HTTP que levam à negação de serviço, com orientação de remediação.
backend/internal/handler/auth_wechat_oauth.go:1149io.ReadAll(resp.Body) para ler todo o corpo da resposta HTTP upstream na memória sem impor nenhum limite máximo de tamanho antes do buffer.backend/internal/handler/auth_wechat_oauth.go:1124A função sinalizada e mais de 50 linhas de contexto circundante foram revisadas. O sink é o helper de troca de token exchangeWeChatOAuthCode(). Esta função constrói uma requisição GET para o WeChat, a envia usando um http.Client simples e então lê todo o corpo da resposta com .
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)
}
...
}
Evidências:
backend/internal/handler/auth_wechat_oauth.go:1142 cria client := &http.Client{Timeout: 30 * time.Second}.backend/internal/handler/auth_wechat_oauth.go:1149 executa body, err := io.ReadAll(resp.Body).io.LimitReader, nenhuma verificação de ContentLength e nenhuma chamada de função helper está presente antes do buffer do corpo da resposta.Análise: A única proteção aqui é um limite de tempo (Timeout: 30 * time.Second), não um limite de memória. Um peer remoto ainda pode acionar grandes alocações de memória antes que o EOF seja alcançado. A acessibilidade deste sink a partir de rotas de produção foi então rastreada.
O registro da rota de autenticação e o handler OAuth do WeChat foram revisados. A rota está registrada no grupo público /auth, não em um grupo protegido por JWT. O handler de callback recebe os parâmetros de consulta code e state fornecidos pelo usuário antes de invocar o fluxo sinalizado.
// 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)
}
Evidências:
backend/internal/server/routes/auth.go:27-28 coloca essas rotas no grupo público de roteadores /auth.backend/internal/server/routes/auth.go:73 registra auth.GET("/oauth/wechat/start", h.Auth.WeChatOAuthStart).backend/internal/server/routes/auth.go:80 registra auth.GET("/oauth/wechat/callback", h.Auth.WeChatOAuthCallback).backend/internal/handler/auth_wechat_oauth.go:160-162 lê code e state da requisição HTTP.backend/internal/handler/auth_wechat_oauth.go:206 chama fetchWeChatOAuthIdentity(c.Request.Context(), cfg, code).backend/internal/handler/auth_wechat_oauth.go:105-147 mostra a etapa de pré-processamento normal que define um cookie de estado e redireciona o navegador para o fluxo OAuth.Análise: Este é código de produção ativo, acessível a partir de um callback GET público. O cookie de estado fornece proteção OAuth-CSRF, mas não limita o tamanho da resposta HTTP que chega a jusante após a troca de token. A cadeia de chamadas interna e todos os chamadores de produção do sink foram então acompanhados.
exchangeWeChatOAuthCode()A cadeia de chamadas interna entre o handler e as funções helper foi rastreada. O callback principal alcança o sink através de um helper (fetchWeChatOAuthIdentity), enquanto o callback de pagamento alcança o mesmo sink diretamente.
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
}
Evidências:
backend/internal/handler/auth_wechat_oauth.go:1112-1117 mostra fetchWeChatOAuthIdentity() chamando exchangeWeChatOAuthCode() e depois fetchWeChatUserInfo().backend/internal/handler/auth_wechat_oauth.go:206 é onde o callback OAuth principal chama fetchWeChatOAuthIdentity().backend/internal/handler/auth_wechat_oauth.go:438 é onde o callback OAuth de pagamento chama exchangeWeChatOAuthCode() diretamente.Análise: O sink sinalizado não é código morto. Uma busca de chamadores em todo o projeto resolve dois caminhos de entrada de produção para o mesmo helper: o callback padrão de login/vínculo do WeChat e o callback de pagamento do WeChat. Quaisquer sanitizadores, validadores ou proteções de tamanho em nível de framework presentes neste caminho de resposta foram então verificados.
O código foi pesquisado por padrões de limitação de tamanho do corpo da resposta, e o helper compartilhado já usado em outros lugares para leituras upstream limitadas foi revisado. Esse helper usa io.LimitReader(..., maxBytes+1) e gera erro explicitamente quando o limite é excedido, mas o código OAuth do WeChat não o usa.
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))
Evidências:
backend/internal/service/upstream_response_limit.go:26-40 define readUpstreamResponseBodyLimited() usando io.LimitReader(reader, maxBytes+1).backend/internal/service/upstream_response_limit.go:49-61 define ReadUpstreamResponseBody() como um wrapper compartilhado de leitura limitada.backend/internal/config/config.go:55-58 define DefaultUpstreamResponseReadMaxBytes.backend/internal/service/crs_sync_service.go:1166 usa io.ReadAll(io.LimitReader(resp.Body, 1<<20)) para ler uma resposta upstream.backend/internal/service/crs_sync_service.go:1201 usa io.ReadAll(io.LimitReader(resp.Body, 5<<20)) para outra resposta upstream.backend/internal/handler/auth_wechat_oauth.go:1149 ainda usa um io.ReadAll(resp.Body) bruto.Análise: O projeto já reconheceu a necessidade de limitar os tamanhos dos corpos de resposta upstream, e tanto implementações de limite ad-hoc quanto compartilhadas estão em uso. O caminho OAuth do WeChat sinalizado ignora essas proteções completamente. O endpoint remoto foi então verificado para determinar se é fixo e se isso altera a conclusão.
As definições de constantes e o código adjacente de busca de informações do usuário foram revisados. O endpoint de token OAuth é codificado para o WeChat e não é fornecido pelo usuário, mas o conteúdo do corpo da resposta ainda é dados de rede remotos e é lido sem nenhum limite de tamanho. Uma segunda leitura upstream ilimitada existe no mesmo arquivo para a requisição de informações do usuário.
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
}
}
Evidências:
backend/internal/handler/auth_wechat_oauth.go:52-54 codifica endpoints upstream para URLs do WeChat.backend/internal/handler/auth_wechat_oauth.go:1197 realiza uma segunda leitura io.ReadAll(resp.Body) ilimitada dentro de fetchWeChatUserInfo().backend/internal/server/middleware/backend_mode_guard.go:38-55 permite explicitamente /auth/oauth/wechat/callback e /auth/oauth/wechat/payment/callback mesmo quando o modo backend está habilitado.Análise: O hostname fixo do WeChat reduz o controle de um atacante sobre a seleção de endpoints, mas não adiciona nenhum limite de tamanho de resposta. A fraqueza permanece porque a aplicação armazena em buffer bytes arbitrários de resposta remota na memória sem um limite superior. Este é código de handler de produção, não código de teste, demonstração ou morto.
exchangeWeChatOAuthCode() emite uma requisição HTTP de saída e então chama io.ReadAll(resp.Body) em backend/internal/handler/auth_wechat_oauth.go:1142-1149 sem proteção de tamanho.backend/internal/server/routes/auth.go:73 e backend/internal/server/routes/auth.go:80 registram as rotas públicas de início/callback do WeChat, e backend/internal/handler/auth_wechat_oauth.go:206 direciona o fluxo de callback através de fetchWeChatOAuthIdentity().backend/internal/service/upstream_response_limit.go:26-40 e outros serviços usam io.LimitReader em backend/internal/service/crs_sync_service.go:1166 e backend/internal/service/crs_sync_service.go:1201, mas a função sinalizada não usa nenhum deles.backend/internal/handler/auth_wechat_oauth.go, está registrado em backend/internal/server/routes/auth.go:73-82, e o middleware de modo backend ainda permite os caminhos de callback em backend/internal/server/middleware/backend_mode_guard.go:38-55.Vulnerabilidade Verdadeira
Evidências-Chave:
backend/internal/handler/auth_wechat_oauth.go:1149 usa body, err := io.ReadAll(resp.Body) para ler toda a resposta de token upstream após definir apenas um timeout — sem limite de tamanho.backend/internal/server/routes/auth.go:73-82 expõe /oauth/wechat/callback e /oauth/wechat/payment/callback, e ambas as rotas alcançam o helper sinalizado via backend/internal/handler/auth_wechat_oauth.go:206 e backend/internal/handler/auth_wechat_oauth.go:438.backend/internal/service/upstream_response_limit.go:26-40 e backend/internal/service/crs_sync_service.go:1166 mostram que o código já usa io.LimitReader para leituras upstream limitadas — o código OAuth do WeChat simplesmente não usa.resp.Body com io.LimitReader(..., maxBytes+1) e rejeite corpos que excedam o limite.backend/internal/handler/auth_wechat_oauth.go:1197, que tem o padrão ilimitado idêntico.backend/internal/service/upstream_response_limit.go:26-40 para que o fluxo OAuth seja consistente com a forma como o restante do código lida com respostas upstream.backend/internal/handler/auth_wechat_oauth.go:206) e o callback de pagamento (backend/internal/handler/auth_wechat_oauth.go:438).