🐕

Go でログに機密情報を出力していないか静的解析するツールを作った

に公開

はじめに

アプリケーション開発において、デバッグやモニタリングのためにログを出力することは一般的です。しかし、うっかりパスワードやAPIキーなどの機密情報をログに含めてしまうと、重大なセキュリティインシデントにつながる可能性があります。

本記事では、Go で書かれたコードを静的解析し、機密情報がログに出力されていないかチェックするツール leakhound を紹介します。

https://github.com/nilpoona/leakhound

leakhound とは

leakhoundは、Go の静的解析を利用して、sensitive:"true" タグが付けられた構造体フィールドが log/slog で誤ってログ出力されていないかを検出するツールです。

名前の由来は、リークを嗅ぎつける猟犬(bloodhound)のように、コード内の潜在的なデータ漏洩リスクを追跡することから来ています。

主な特徴

  • 静的解析によるゼロランタイムコスト: 実行時のパフォーマンス影響なし
  • 予防的なアプローチ: コードレビュー段階で問題を検出

対応環境

  • 対象言語: Goで実装されたアプリケーション
  • 対応ロギングパッケージ: log/slog, fmt, log

なぜ作ったのか

実際の開発現場では、以下のような問題がしばしば発生します。

type User struct {
    ID       int
    Name     string
    Password string
}

// デバッグ中にうっかり...
slog.Info("user login", "user", user)  // Passwordも一緒に出力されてしまう!

セキュリティは多層的な防御が重要です。ランタイムでのマスキング処理やログの暗号化など、さまざまな対策がありますが、開発の早い段階で問題を検出することで、より安全性を高められます。

leakhoundは静的解析による早期発見を目的としたツールです。他の防御策と組み合わせることで、多層的なセキュリティ対策を実現できます。

インストール

Go 1.24以降では、tool ディレクティブでの管理が可能です。

# プロジェクトに追加(Go 1.24+)
go get -tool github.com/nilpoona/leakhound

# 実行
go tool leakhound ./...

Go 1.23以前の場合や、グローバルにインストールしたい場合は以下の方法も利用できます。

go install github.com/nilpoona/leakhound@latest
leakhound ./...

使い方

1. 機密情報に sensitive タグを付ける

まず、機密情報を含むフィールドに sensitive:"true" タグを付けます。

type User struct {
    ID       int
    Name     string
    Password string `sensitive:"true" json:"-"`
    APIKey   string `sensitive:"true" json:"-"`
    Email    string `sensitive:"true" json:"email"`
}

type Config struct {
    Host     string
    Port     int
    Token    string `sensitive:"true"`
    Database string
}

2. leakhound を実行

# カレントディレクトリ配下を検査
go tool leakhound ./...

# 特定のパッケージを検査
go tool leakhound ./internal/...

3. 検出例

以下のようなコードは検出されます。

// ❌ 直接フィールドアクセス
slog.Info("msg", "pass", user.Password)

// ❌ slog.String でラップしても検出
slog.Info("msg", slog.String("pass", user.Password))

// ❌ ポインタ経由でも検出
userPtr := &user
slog.Info("msg", "pass", userPtr.Password)

// ❌ sensitive フィールドを含む構造体全体
slog.Info("user data", user)
slog.Info("user data", slog.Any("data", user))

検出できないケース

静的解析の性質上、以下のようなケースは検出が困難です。

// ❌ 関数を経由する場合
func logPassword(p string) {
    slog.Info("msg", "pass", p)
}
logPassword(user.Password)  // 検出困難

// ❌ リフレクション経由
val := reflect.ValueOf(user).FieldByName("Password")
slog.Info("msg", "pass", val.Interface())

// ❌ interface{} 経由
var data interface{} = user.Password
slog.Info("msg", "pass", data)

これらのケースでは、型情報が失われるため、静的解析では追跡できません。機密情報を直接ログ関数に渡していない場合は検出できない点にご注意ください。

実装のポイント

leakhound は Go の golang.org/x/tools/go/analysis パッケージを使用して実装されています。

主な処理フローは以下の通りです。

  1. 構造体フィールドの解析: sensitive:"true" タグが付いたフィールドを特定
  2. ログ関数呼び出しの検出: log/slog, fmt, log パッケージの関数呼び出しを探索
  3. 引数の追跡: 呼び出し時の引数が機密フィールドを参照していないかチェック
  4. 診断情報の生成: 問題が見つかった場合、該当箇所を報告

特に工夫した点は、以下のようなケースでも検出できるようにしたことです。

  • ポインタを経由したフィールドアクセス
  • slog.String(), slog.Any() などのヘルパー関数でラップされたケース
  • 構造体全体をログ出力するケース(sensitiveフィールドを含む場合)

今後の展開

現在は log/slog, fmt, log に対応していますが、今後は以下の拡張を検討しています。

  • logrus, zap など他のロギングライブラリへの対応
  • より高度な型追跡(関数を跨いだ解析)
  • カスタムタグ名の指定
  • 誤検知の除外設定

まとめ

leakhound は、Goでのセキュアなログ出力を支援する静的解析ツールです。

静的解析のメリット:

  • ✅ コードレビュー段階で問題を発見
  • ✅ ランタイムのパフォーマンス影響ゼロ

機密情報の漏洩は、一度発生すると取り返しのつかない問題になります。開発の早い段階でこうした問題を防ぐために、ぜひ leakhound を活用してみてください。

コントリビューションも歓迎しています!バグ報告や機能追加の提案がありましたら、GitHub の Issues でお知らせください。

https://github.com/nilpoona/leakhound

Discussion