pprofデバッグエンドポイントの露出

pprofデバッグエンドポイントの露出

説明

net/http/pprof をインポートすると、http.DefaultServeMux に /debug/pprof/ のハンドラーが自動登録されます。名前付きの通常のインポートでも同じです。この既定のmuxを http.ListenAndServe(":8080", nil) などで全インターフェースへ提供し、別途アクセスを制御しないと、そのポートへ到達できるユーザーが認証なしでプロファイルを取得できます。

/debug/pprof、/debug/pprof/goroutine、/debug/pprof/heap などから、goroutine、メモリの状態、内部パス、一部のリクエスト情報が露出する可能性があります。これ自体がコード実行ではなくても、ハンドラーや秘密の値の推測、サービス構造の分析など、追加の攻撃に利用されるおそれがあります。

想定される影響

  • goroutine、heap、threadcreateなどの情報から内部構造や実行状態が明らかになる可能性があります。
  • メモリ使用量、処理パターン、一部の内部パスなどのインフラ情報が漏れるおそれがあります。
  • 攻撃者がエンドポイントや性能上のボトルネックを調べ、サービス拒否などの追加攻撃を準備する可能性があります。

対処方法

  • pprofは localhost やループバックにバインドし、外部サービスには別の http.NewServeMux() を使ってください。
  • 必要なpprofハンドラーを内部用muxへ登録してください。インポートだけで既定のmuxにも登録される点に注意してください。
  • mTLS、VPN、内部プロキシなどでアクセスを制限してください。パスワード認証を使う場合も、検証されたTLSで保護してください。
  • 本番で不要なpprofのインポートやサーバー設定を削除し、必要な診断だけを制限付きで有効にしてください。
  • ファイアウォール、セキュリティグループ、Ingressで外部アクセスを遮断し、プロキシが内部pprofを再公開しないか確認してください。

例

変更前

go
package main

import (
    "log"
    "net/http"

    _ "net/http/pprof" // pprofハンドラーを既定のmuxへ自動登録
)

func main() {
    // pprofとアプリケーションのハンドラーが既定のmux(nil)を共有
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello World"))
    })

    // 全インターフェース(:8080)で待ち受け、到達可能なクライアントへ/debug/pprofも公開
    log.Fatal(http.ListenAndServe(":8080", nil))
}

変更後

go
package main

import (
    "log"
    "net/http"
    "net/http/pprof"
)

// 内部専用プロファイルサーバーの設定例
func startInternalPprofServer() {
    mux := http.NewServeMux()

    // 内部サーバー用muxへpprofハンドラーを登録
    mux.HandleFunc("/debug/pprof/", pprof.Index)
    mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
    mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
    mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
    mux.HandleFunc("/debug/pprof/trace", pprof.Trace)

    // localhostなどのローカルアドレスだけにバインド
    go func() {
        log.Println("internal pprof listening on localhost:6060")
        if err := http.ListenAndServe("localhost:6060", mux); err != nil {
            log.Println("pprof server stopped:", err)
        }
    }()
}

func main() {
    // 内部pprofを起動。本番ではファイアウォールやセキュリティグループでアクセスを制御
    startInternalPprofServer()

    // pprofは既定muxへ自動登録されるため、サービス用muxを分ける
    publicMux := http.NewServeMux()
    publicMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello World"))
    })

    // pprofを登録していないmuxで外部サービスのポートを提供
    log.Fatal(http.ListenAndServe(":8080", publicMux))
}

解説:

  • 変更前: 既定のmuxに登録されたpprofをアプリケーションと同じポートで提供するため、そのポートへ到達できるユーザーは別の認証なしでプロファイルを取得できます。
  • 変更後: インポートによる既定muxへの登録は残りますが、外部サーバーは別の publicMux だけを使います。プロファイル用muxは localhost:6060 で待ち受けます。ローカルユーザー、同じネットワーク名前空間のプロセス、再公開するプロキシも考慮してアクセスを制御してください。

参考資料