5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

MySQLの実行エラーでDatadogに機密情報が流出しないように気をつけましょう

Posted at

これは ZOZO Advent Calendar 2025 カレンダー、シリーズ 12 の 14 日目の記事です。

はじめに

アプリケーションのモニタリングツールにDatadogを採用している企業は多いかと思います。
本記事ではMySQLのコマンド実行でエラーが発生した際に、Datadogに意図せず機密情報を送信してしまっていないかの確認とその対策について紹介したいと思います。
(一例としてMySQLを取り上げてますが、他のDBでも同じケースに当てはまる可能性は高いと思います)

なお本記事では、Datadogの概要やGoでのDatadogの使用方法については本筋ではないため割愛しています。

機密情報の漏洩

GoでMySQLのINSERT文を複数回実行し、重複エラーの内容をDatadogでトレーシングしてみます。
DBのemailフィールドにはNOT NULL制約が付与されているものとします。
(サンプルコードなのでハードコーディングなどのご指摘はご容赦いただければと思います🙏)

import (
    "context"
    "database/sql"
    "log"

    "golang.org/x/xerrors"
    "github.com/go-sql-driver/mysql"
    sqltrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/database/sql"
    "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
)

func main() {
    tracer.Start()
    defer tracer.Stop()
    
    ctx := context.Background()
    db, err := newTraceDB()
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    if err := insertUser(ctx, db, 20, "優太", "yuta@example.com"); err != nil {
		log.Fatal(err)
	}

    // 重複エラーを発生させるために同じemailの値で再びINSERTする
    if err := insertUser(ctx, db, 21, "祐太", "yuta@example.com"); err != nil {
		log.Fatal(err)
	}
}

func newTraceDB() (*sql.DB, error) {
	sqltrace.Register("mysql", &mysql.MySQLDriver{}, sqltrace.WithServiceName("sample-service"))
	db, err := sqltrace.Open("mysql", "user:password@tcp(host:3306)/sample_db")
	if err != nil {
		return nil, xerrors.Errorf("failed to open mysql: %v", err)
	}
    return db, nil
}

func insertUser(ctx context.Context, db *sql.DB, age int, name, email string) error {
    query := "INSERT INTO users (age, name, email) VALUES (?, ?, ?)"
	_, err := db.ExecContext(ctx, query, age, name, email)
	if err != nil {
		return xerrors.Errorf("failed to insert users: %v", err)
	}
    return nil
}

Datadog APMを確認すると、ユニークキーが重複していてINSERTに失敗しているという内容のエラーが表示されます。
(会社の開発環境を使用しているため色々伏せてます)

datadog1.png

赤文字のエラーメッセージにはDuplicate entry 'xxxx@zozo.com' for key 'xxxx(フィールド名)'とあり、INSERTしようとしたレコードの中身が意図せず表示されていることが確認できます。
これはいけませんね。

対策

dd-trace-goライブラリで良さげな機能がないか探してみます。
MySQLのエラーメッセージを表示しつつ、機密情報のみをマスクできるのが理想ですが、2022/12/13時点ではこれを実現できる機能は提供されてなさそうでした。
そのため今回はWithErrorCheck()を使って対策していこうと思います。

WithErrorCheck specifies a function fn which determines whether the passed error should be marked as an error. The fn is called whenever a database/sql operation finishes with an error

WithErrorCheck は、渡されたエラーをエラーとしてマークすべきかどうかを判断する関数 fn を指定します。データベース/SQL 操作がエラーで終了するたびに、fn が呼び出されます。

要するに、SQLエラーが発生したらそれをエラーとしてDatadogに記録するかどうかの定義を、関数でカスタマイズできるってことですね。
例えば重複エラーは想定済みなのでエラーとして記録しない場合は、以下のように実装できます。

sqltrace.WithErrorCheck(func(err error) bool {
    if err == nil {
        return false
    }
    if strings.Contains(err.Error(), "duplicate") {
        return false // Datadogではエラー扱いにしない
    }
    return true // それ以外はエラー扱い
})

機密情報が流出される可能性のあるエラーを一つ一つハンドリングしてもいいですが、考慮漏れはもちろん、将来的にエラーが増えたり変わったりすることもないとは言えないので、以下のようにMySQLのエラーを全て送信しないようにしておくのがベターかと思います。

func newTraceDB() (*sql.DB, error) {
    sqltrace.Register("mysql", &mysql.MySQLDriver{}, sqltrace.WithServiceName("sample-service"), sqltrace.WithErrorCheck(func(_ error) bool {
    		return false // MySQL実行で発生したエラーは全てエラーとして扱わない
    }))
    db, err := sqltrace.Open("mysql", "user:password@tcp(host:3306)/sample_db")
	if err != nil {
		return nil, xerrors.Errorf("failed to open mysql: %v", err)
	}
    return db, nil
}

Datadogを確認すると、重複エラーが発生してもエラーの詳細がトレースされていないことが確認できます。
datadog2.png

このようにWithErrorCheck()はエラーか否かを判断する用途なので、現時点だと実行エラーの中身を潰してしまう手段しか取れなさそうかなという所感になります。

ちなみに他のエラー監視ツールのSentryでは、初期化時にBeforeSendフィールドを活用すれば該当箇所だけ柔軟にマスクできます。
Datadogもこういった形で実装できれば嬉しいんですけどね...

err := sentry.Init(sentry.ClientOptions{
            Dsn:         "sample-dsn",
			BeforeSend: func(event *sentry.Event, _ *sentry.EventHint) *sentry.Event {
				// 対象ヘッダーの値をXXXXでマスクする
				maskedHeaderKeys := []string{"sample-header1", "sample-header2"}
				for _, k := range maskedHeaderKeys {
					event.Request.Headers[strings.Tolower(k)] = "XXXXX" 
				}
				event.Request.Data = "" // リクエストボディを空にする
				return event
			},
		})

↓Sentryのエラー詳細ページで対象ヘッダーの値がマスクされている

まとめ

近年、ランサムウェアなどのサイバー攻撃を受け、事業を停止せざるを得なくなった事案が目に付きます。そのため、こういった記事を通じて少しでもIT業界のセキュリティに対する意識向上に貢献できたらなと思います。

ではまた。

5
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?