Skip to content
KitploitKITPLOIT
도구블로그
제출
도구블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

··피드·문의·개인정보·© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
CVE-2026-51416 — WeChat OAuth 핸들러의 특정 CVE를 분석하여, 무제한 HTTP 응답 읽기로 인한 서비스 거부(DoS)를 식별하고, 이를 해결하기 위한 지침을 제공합니다. | Kitploit
도구/GitHubGitHub/renio-wow/cve-2026-51416
Static AnalysisVulnerability AnalysisCode AnalysisWeb Security
GitHubrenio-wow/cve-2026-51416

CVE-2026-51416

WeChat OAuth 핸들러의 특정 CVE를 분석하여, 무제한 HTTP 응답 읽기로 인한 서비스 거부(DoS)를 식별하고, 이를 해결하기 위한 지침을 제공합니다.

저장소 보기
4개월 전아직 검토되지 않음

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

1. 요약

  • 취약점 유형: 서비스 거부(Denial of Service) / 무제한 HTTP 응답 읽기([CWE-400])
  • 탐지 위치: backend/internal/handler/auth_wechat_oauth.go:1149
  • 취약점 설명: WeChat OAuth 토큰 교환 프로세스는 io.ReadAll(resp.Body)를 사용하여 업스트림 HTTP 응답 본문 전체를 메모리에 읽어들이며, 버퍼링 전에 최대 크기 제한을 적용하지 않습니다.

2. 분석

1단계: backend/internal/handler/auth_wechat_oauth.go:1124의 탐지된 싱크(Sink) 검토

탐지된 함수와 주변 50줄 이상의 컨텍스트를 검토했습니다. 싱크는 토큰 교환 헬퍼 함수인 exchangeWeChatOAuthCode()입니다. 이 함수는 WeChat에 GET 요청을 구성하고, 일반 http.Client를 사용하여 전송한 다음, io.ReadAll(resp.Body)로 응답 본문 전체를 읽습니다.

root@kitploit:~
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에 도달하기 전에 여전히 대용량 메모리 할당을 유발할 수 있습니다. 이후 프로덕션 라우트에서 이 싱크에 도달 가능한지 추적했습니다.

2단계: 공개 OAuth 콜백 라우트의 주요 요청 경로 추적

인증 라우트 등록과 WeChat OAuth 핸들러를 검토했습니다. 이 라우트는 JWT로 보호되는 그룹이 아닌 공개 /auth 그룹에 등록되어 있습니다. 콜백 핸들러는 탐지된 플로우를 호출하기 전에 사용자가 제공한 code 및 state 쿼리 파라미터를 수신합니다.

root@kitploit:~
// 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)
root@kitploit:~
// 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
    }
    ...
}
root@kitploit:~
// 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에서 HTTP 요청으로부터 code 및 state를 읽습니다.
  • 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 응답의 크기를 제한하지는 않습니다. 이후 내부 호출 체인과 싱크의 모든 프로덕션 호출자를 추적했습니다.

3단계: exchangeWeChatOAuthCode()로 이어지는 모든 프로덕션 호출자 추적

핸들러와 헬퍼 함수 사이의 내부 호출 체인을 추적했습니다. 주요 콜백은 헬퍼(fetchWeChatOAuthIdentity)를 통해 싱크에 도달하고, 결제 콜백은 동일한 싱크에 직접 도달합니다.

root@kitploit:~
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)
    ...
}
root@kitploit:~
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 결제 콜백입니다. 이후 이 응답 경로에 존재하는 샌티타이저, 검증기, 또는 프레임워크 수준의 크기 가드를 확인했습니다.

4단계: 코드베이스의 크기 제한 및 기존 보안 패턴 확인

응답 본문 크기 제한 패턴에 대해 코드베이스를 검색하고, 다른 곳에서 이미 사용 중인 공유 헬퍼를 검토했습니다. 해당 헬퍼는 io.LimitReader(..., maxBytes+1)를 사용하며 제한을 초과할 때 명시적으로 오류를 반환하지만, WeChat OAuth 코드는 이를 사용하지 않습니다.

root@kitploit:~
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
}
root@kitploit:~
// DefaultUpstreamResponseReadMaxBytes is the default read cap for upstream non-streaming response bodies.
const DefaultUpstreamResponseReadMaxBytes int64 = 128 * 1024 * 1024
root@kitploit:~
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에서 io.LimitReader(reader, maxBytes+1)를 사용하는 readUpstreamResponseBodyLimited()를 정의합니다.
  • 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 경로는 이러한 보호를 완전히 우회합니다. 이후 원격 엔드포인트가 고정되어 있는지, 그리고 그것이 결론을 바꾸는지 확인했습니다.

5단계: 신뢰 경계 및 인접 코드 컨텍스트 검증

상수 정의와 인접한 사용자 정보 가져오기 코드를 검토했습니다. OAuth 토큰 엔드포인트는 WeChat으로 하드코딩되어 있으며 사용자가 제공하지 않지만, 응답 본문 콘텐츠는 여전히 원격 네트워크 데이터이며 크기 제한 없이 읽힙니다. 동일한 파일에 사용자 정보 요청에 대한 두 번째 무제한 업스트림 읽기가 존재합니다.

root@kitploit:~
var (
    wechatOAuthAccessTokenURL = "https://api.weixin.qq.com/sns/oauth2/access_token"
    wechatOAuthUserInfoURL    = "https://api.weixin.qq.com/sns/userinfo"
)
root@kitploit:~
body, err := io.ReadAll(resp.Body)
if err != nil {
    return nil, fmt.Errorf("read wechat userinfo response: %w", err)
}
root@kitploit:~
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 URL로 하드코딩합니다.
  • backend/internal/handler/auth_wechat_oauth.go:1197에서 fetchWeChatUserInfo() 내부에서 두 번째 무제한 io.ReadAll(resp.Body)를 수행합니다.
  • backend/internal/server/middleware/backend_mode_guard.go:38-55에서 백엔드 모드가 활성화된 경우에도 /auth/oauth/wechat/callback 및 /auth/oauth/wechat/payment/callback을 명시적으로 허용합니다.

분석: 고정된 WeChat 호스트 이름은 공격자의 엔드포인트 선택 제어를 줄이지만 응답 크기 제한을 추가하지는 않습니다. 애플리케이션이 임의의 원격 응답 바이트를 상한 없이 메모리에 버퍼링하기 때문에 취약점은 여전히 존재합니다. 이는 테스트, 데모 또는 죽은 코드가 아닌 프로덕션 핸들러 코드입니다.

결정 경로

  • Q1: 코드가 원격으로 공급된 데이터의 무제한 읽기를 수행합니까? → 예. exchangeWeChatOAuthCode()는 아웃바운드 HTTP 요청을 발행한 다음 backend/internal/handler/auth_wechat_oauth.go:1142-1149에서 크기 가드 없이 io.ReadAll(resp.Body)를 호출합니다.
  • Q2: 싱크가 실제 프로덕션 요청 경로에서 도달 가능합니까? → 예. backend/internal/server/routes/auth.go:73 및 backend/internal/server/routes/auth.go:80에서 공개 WeChat 시작/콜백 라우트를 등록하고, backend/internal/handler/auth_wechat_oauth.go:206에서 콜백 플로우를 fetchWeChatOAuthIdentity()로 라우팅합니다.
  • Q3: 읽기 전에 최대 응답 본문 크기를 강제하는 샌티타이저, 검증기, 또는 프레임워크/헬퍼 함수가 있습니까? → 아니요. 공유 제한 헬퍼가 backend/internal/service/upstream_response_limit.go:26-40에 존재하고 다른 서비스는 backend/internal/service/crs_sync_service.go:1166 및 backend/internal/service/crs_sync_service.go:1201에서 를 사용하지만, 탐지된 함수는 이들 중 어느 것도 사용하지 않습니다.

3. 결론

실제 취약점(True Vulnerability)

주요 근거:

  • 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 코드는 단순히 이를 사용하지 않습니다.

4. 수정 방안

  • WeChat 토큰 응답을 버퍼링하기 전에 명시적인 최대 응답 본문 크기를 추가하십시오 — 예를 들어, resp.Body를 io.LimitReader(..., maxBytes+1)로 감싸고 제한을 초과하는 본문을 거부하십시오.
  • 동일한 무제한 패턴을 가진 인접한 사용자 정보 읽기(backend/internal/handler/auth_wechat_oauth.go:1197)에도 동일한 수정을 적용하십시오.
  • backend/internal/service/upstream_response_limit.go:26-40의 기존 경계 읽기 패턴을 재사용하여 OAuth 플로우가 코드베이스의 나머지 부분이 업스트림 응답을 처리하는 방식과 일관되도록 하십시오.
  • 수정 후 두 프로덕션 호출 경로를 다시 검증하십시오: 주요 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)를 사용합니다.
  • io.LimitReader
  • Q4: 코드가 죽은 코드, 테스트 전용, 또는 프로덕션에서 제외됩니까? → 아니요. 코드는 프로덕션 핸들러 파일 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에서 콜백 경로를 허용합니다.
  • → 진양성(True Positive)