backend/internal/handler/auth_wechat_oauth.go:1149io.ReadAll(resp.Body) を使用して上流のHTTPレスポンスボディ全体をメモリに読み込みますが、バッファリング前に最大サイズ制限を一切適用していません。backend/internal/handler/auth_wechat_oauth.go:1124 で調査フラグ付き関数と周囲50行以上のコンテキストを確認しました。シンクはトークン交換ヘルパー exchangeWeChatOAuthCode() です。この関数はWeChatへのGETリクエストを構築し、プレーンな 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ハンドラーを確認しました。ルートはJWT保護グループではなく、公開 /auth グループの下に登録されています。コールバックハンドラーは、フラグ付きフローを呼び出す前に、ユーザー指定の 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 は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レスポンスのサイズを制限するものではありません。次に、内部呼び出しチェーンとシンクのすべての本番呼び出し元を追跡しました。
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() を直接呼び出す場所。分析: フラグ付きシンクはデッドコードではありません。プロジェクト全体の呼び出し元検索により、同じヘルパーへの2つの本番エントリパス(標準の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 は 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パスはこれらの保護を完全にバイパスしています。次に、リモートエンドポイントが固定されているかどうか、およびそれが結論を変えるかどうかを確認しました。
定数定義と隣接するユーザー情報取得コードを確認しました。OAuthトークンエンドポイントはWeChatにハードコードされており、ユーザー指定ではありませんが、レスポンスボディの内容は依然としてリモートネットワークデータであり、サイズ制限なしで読み取られます。同じファイル内にユーザー情報リクエスト用の2つ目の無制限の上流読み取りが存在します。
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 URLにハードコード。backend/internal/handler/auth_wechat_oauth.go:1197 は fetchWeChatUserInfo() 内で2つ目の無制限の io.ReadAll(resp.Body) を実行。backend/internal/server/middleware/backend_mode_guard.go:38-55 はバックエンドモードが有効な場合でも /auth/oauth/wechat/callback と /auth/oauth/wechat/payment/callback を明示的に許可。分析: 固定されたWeChatホスト名は攻撃者のエンドポイント選択の制御を減らしますが、レスポンスサイズ制限を追加するものではありません。アプリケーションが任意のリモートレスポンスバイトを上限なしでメモリにバッファリングするため、脆弱性は残ります。これは本番ハンドラーコードであり、テスト、デモ、またはデッドコードではありません。
exchangeWeChatOAuthCode() は送信HTTPリクエストを発行し、backend/internal/handler/auth_wechat_oauth.go:1142-1149 でサイズガードなしに io.ReadAll(resp.Body) を呼び出します。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 に存在し、他のサービスは backend/internal/service/crs_sync_service.go:1166 と backend/internal/service/crs_sync_service.go:1201 で を使用していますが、フラグ付き関数はそれらのいずれも使用していません。真の脆弱性
主要な証拠:
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) を使用。io.LimitReaderbackend/internal/handler/auth_wechat_oauth.go に存在し、backend/internal/server/routes/auth.go:73-82 で登録され、バックエンドモードミドルウェアは依然として backend/internal/server/middleware/backend_mode_guard.go:38-55 でコールバックパスを許可しています。