MoTLab -GO Inc. Engineering Blog-MoTLab -GO Inc. Engineering Blog-

テストで秘匿情報のマスク漏れをチェックしよう

テストServerSideGo自動化log
June 21, 2022

テストで秘匿情報のマスク漏れをチェックしよう


APIサーバの秘匿情報マスクの仕組み

APIサーバでは個々のリクエストについて、リクエストを受信してから、レスポンスを返すまで、アドレス、ヘッダー、リクエストパラメータ、内部処理など、様々なタイミングでログに記録されています。情報漏えいを防ぐために、このログへのアクセスは、Googl Cloud Platform projectのアクセス制限下にあり、限られたエンジニアのみ閲覧権限が与えられています。加えて、秘匿情報はログに平文で出力しない、ことを規約として定めています。また、開発環境と本番環境を分離し、開発環境でログのマスク漏れを検知する仕組みを導入し、本番リリース前に検知・対応できるような体制をとっています。

ログデータはGoogl Cloud Loggingで管理され、分析、モニタリング、調査などに活用されます。ここで、ログは調査で利用することもあり、利便性の観点も踏まえて、全ての値ではなく秘匿情報に限りマスクしています。あらかじめマスクするキー文字列を定義し、ログを出力する直前に、ログテキスト内でマッチするキーを検索、値をマスク文字列に置換してから出力します。これは一般的なキーマッチです。また、ログに出力される全てタイミングで、秘匿情報が一括してマスクされるよう定義・処理しています。

マスク漏れが起きる原因

キーマッチによるマスク処理ですので、キーが正しく定義されていない場合、マスク漏れが発生する恐れがあります。例えば、次のようなケースで、マスク漏れ・情報漏洩が発生する危険性が高くなります。

  • 新しい名前の秘匿フィールドを追加したけど、マスクキーを登録し忘れていた
  • 登録されているキーの定義と、追加したフィールド名がアンマッチだった
  • マスク漏れ検知に気が付かなかった
  • コードレビューで気が付かなかった

これらのケースはヒューマンエラーです。

テストでマスク漏れをチェックしよう

人に依らず、マスク漏れ発生をチェックし、コーディングからリリースまでのフローを一時停止させる仕組みとして、マスク漏れをチェックするユニットテストを考えました。

masking_field_struct.go

1 package maskingtest
2
3 // テスト用構造体
4 type MaskingFieldTestStruct struct{
5    ///////////// 秘匿フィールドを追加する ///////////////
6 }
  1. 秘匿情報を格納する構造体のフィールド(秘匿フィールド)に、カスタムタグを定義
  2. 秘匿フィールドだけを持つテスト用構造体を作成
  3. カスタムタグでコードを検索し秘匿フィールドだけをユニークに抽出
  4. テスト用構造体にユニークな秘匿フィールドを追加
  5. テスト用構造体に初期値を入れて、ログ出力用のログテキストに変換
  6. マスキング処理をかける
  7. マスクテキストの値にマスク文字以外が入っていたらエラー

コードのサンプルです。

1
2 // 例えば、このような構造体の秘匿フィールドを、テスト用構造体のフィールドに追加する
3 type maskingFiledTestString struct {
4	  KanaName    []string `masking:"true"`
5	  PhoneNumber string   `masking:"true"`
6	  Latitude    string   `masking:"true"`
7 }
8
9 // マスク後のパターン(json形式)
10 var maskingRegexp = regexp.MustCompile(`^[a-zA-Z,_\[\]"\{\}\:\*\s]*$`)
11
12 func TestMaskingFiledStruct(t *testing.T) {
13   // テスト用構造体のインスタンス
14	 obj := maskingtest.MaskingFieldTestStruct{}
15	 // 初期値を入れる(全てマスクされる)
16	 maskingtest.FakeData(&obj)
17	 // ログテキストを取得
18	 tmp, err := json.Marshal(&obj) // ここでは、json形式に
19	 require.NoError(t, err)
20
21     // 正規化
22	 jsonByte := maskingtest.ConvertToSnakeCaseJSON(tmp)
23	 // マスク処理
24	 r := Masking(jsonByte)
25
26	 // 判定
27	 if !maskingRegexp.Match(r) {
28	   t.Errorf("%s", string(r))
29	 }
30	 require.Equal(t, true, maskingRegexp.Match(r))
31 }

考察

どうやるかはケースバイケースでしょうが、秘匿フィールドの検索には、例えば次のようなやり方があります。

  • リフレクションで確実に取得する(golangなら標準パッケージのreflect)
  • grepで簡単に取得する(コメントなどを拾ってしまうと想定外のエラーが発生するかも)

また、カスタムタグの付け方も、ブラックリスト/ホワイトリスト方式があります。ここではブラックリスト方式を前提に書きましたが、ヒューマンエラー「定義するのを忘れてた」を検知、作業を一時停止させるためには、ホワイトリスト方式のほうがフェールセーフです。ホワイトリスト方式の場合、導入時に全ての非秘匿フィールドにタグ打ちするタスクも発生します。

ブラックリスト方式では、「カスタムタグの定義忘れ」というヒューマンエラーを完全に防ぐことはできません。しかし、次の2つのフローがあったとして、

  • 「これは秘匿情報を格納するフィールドだ」->「マスクキーに定義/確認する」->「ログ/検知の通知を目視確認する」
  • 「これは秘匿情報を格納するフィールドだ」->「カスタムタグを付ける」

後者が、マスクキーの定義・ログ・検知の通知など確認箇所が分散しているのに対して、前者は秘匿フィールドの定義とカスタムタグの付与が同じファイル・行に着目すればよいことから、作業漏れが発生しにくい状態にはなるでしょう。

まとめ

テストでマスク漏れをチェックする仕組みについて検討しました。実際のところ、私の場合は心配性なので、結局、

  • 「これは秘匿情報を格納するフィールドだ」->「カスタムタグを付ける」->「マスクキーに定義/確認する」->「ログ/検知の通知を目視確認する」

をやりそうですが。


We're Hiring!

📢
Mobility Technologies ではともに働くエンジニアを募集しています。

興味のある方は 採用ページ も見ていただけると嬉しいです。

Twitter @mot_techtalk のフォローもよろしくお願いします!