Go Flow Levee で機密データのロギングを検出する

## はじめに
セキュリティ上問題となるプログラミングエラーの一つに、誤って機密データをログに記録してしまうというものがあります [^cwe-532]。データ構造やロジックが複雑になるほど、レビューでもこの種のエラーは見落としやすくなります。[Kubernetes](https://github.com/kubernetes/kubernetes) でも何度かこの問題が起きており、[Go Flow Levee](https://github.com/google/go-flow-levee) というツールを使った[静的解析による対策](https://github.com/kubernetes/enhancements/tree/master/keps/sig-security/1933-secret-logging-static-analysis)が導入されています。

[^cwe-532]: [共通脆弱性タイプ一覧 CWE](https://www.ipa.go.jp/security/vuln/CWE.html) では、この問題を [CWE-532: Insertion of Sensitive Information into Log File](https://cwe.mitre.org/data/definitions/532.html) として分類しています。

本記事では、Go 言語用の静的解析ツール「Go Flow Levee」と、Kubernetes での導入事例についてご紹介します。

## Go Flow Levee とは
[Go Flow Levee](https://github.com/google/go-flow-levee) は、Go 言語用の静的解析ツールです。機密データなどの「ソース」が「サニタイザー」を通さずにログ関数などの「シンク」に到達していないかチェックできます。「ソース」をコピーした変数も「テイント」が付与されて伝搬が分析されることで、同様に「シンク」への到達を検出できます。[^taint-propagation]

[^taint-propagation]: Taint propagation analysis (テイント伝搬解析) と呼ばれる手法です。「テイント (taint)」「シンク (sink)」などの用語もこの分野の専門用語になります。

認証情報などの機密データが誤ってログに記録されるようなケースを検出できるほか、ユーザからの入力に対してのテイントチェック([Taint Checking](https://en.wikipedia.org/wiki/Taint_checking)) としての使い方も可能です。

## Go Flow Levee の基本的な使い方
Go Flow Levee の [Quickstart のドキュメント](https://github.com/google/go-flow-levee/blob/master/guides/quickstart.md) をベースに、基本的な使い方をご紹介します。

### 検査するコード
以下の例 ([quickstart.go](https://github.com/google/go-flow-levee/blob/v1.0.0/guides/quickstart/quickstart.go)) では、`Authentication` 構造体が `Password` というフィールドに機密データを保持しています。このコードでは `log.Printf` で、この構造体をログ出力しているため、機密データをログに記録してしまう問題があります。

```go:quickstart.go
package quickstart

import "log"

type Authentication struct {
	Username string
	Password string // 機密データ
}

func authenticate(auth Authentication) (*AuthenticationResponse, error) {
	response, err := makeAuthenticationRequest(auth)
	if err != nil {
        // 機密データがロギングされている
		log.Printf("unable to make authenticated request: incorrect authentication? %v\n", auth)
		return nil, err
	}
	return response, nil
}
```

### 検査の設定
この例では、コードの検査に以下の 2 つの情報を設定します。この情報でテイント伝搬(taint propagation)の分析が行われ、機密データが指定した関数に渡されていないかチェックされます。

- どの型が機密データを含むか (ソース)
    - 例では `Authentication` 構造体の `Password` フィールド
- 機密データをそのまま渡されてはいけない関数 (シンク)
    - 例では `log.Printf` 関数

#### ソースの設定
機密データを含む型を表すソース(`Source`) の指定には、2 種類の方法があります。

- 機密データを含む各フィールドに任意のタグを指定する
    - コードの変更が必要だが、タグで一括に指定できる
- 機密データを含むフィールド名、パッケージ名、型名を記述する
    - コードの変更はないが、個別に指定が必要

#### タグで指定する
タグで指定する場合、まず機密データを表す任意のタグを定義し、設定ファイルに記載します。以下の例では、`datapolicy` のキーに、`secret` という値が設定されていたときに機密データとして扱われます。

```yaml:analyzer_configuration.yaml
FieldTags:
  - Key: datapolicy
    Value: secret
```

機密データのフィールドにこのタグを設定します。

```go
type Authentication struct {
  Username string
  Password string `datapolicy:"secret"`
}
```

#### フィールド名等で記述する
機密データを含むフィールド名、パッケージ名、型名を記述することもできます。外部パッケージなどタグを付与できない場合は、この方法を利用します。`PackageRE`, `TypeRE`, `FieldRE` を使って、正規表現を用いることもできます。

```yaml:analyzer_configuration.yaml
Sources:
  - Package: github.com/google/go-flow-levee/guides/quickstart
    Type: Authentication
    Field: Password
```

この方法ではコードの変更は必要ありませんが、フィールド名などが変わった場合に忘れずこの設定も更新する必要があります。

### シンクの設定
機密データが渡されてはいけない関数をシンク (Sink) として設定します。以下の例では `log` パッケージの `Printf` を対象としています。`PackageRE`、`MethodRE` を使うと正規表現を用いることもできます。

```yaml:analyzer_configuration.yaml
Sinks:
  - Package: log
    Method: Printf
```

### 検査を実行する
まずは go-flow-levee (`levee` バイナリ)をインストールします。

```bash
go get github.com/google/go-flow-levee/cmd/levee
```

前述のサンプルコードが含まれる [github.com/google/go-flow-levee/guides/quickstart](https://github.com/google/go-flow-levee/tree/master/guides/quickstart) を準備しておきます。

```bash
git clone git@github.com:google/go-flow-levee.git
cd go-flow-levee/guides/quickstart
```

`go vet` に `levee` バイナリを指定して実行します。設定は [analyzer_configuration.yaml](https://github.com/google/go-flow-levee/blob/v1.0.0/guides/quickstart/analyzer_configuration.yaml) を参照してください。

```bash
go vet -vettool=$(which levee) -config=$(realpath analyzer_configuration.yaml) ./...
```

問題箇所が検出されました。:tada:

```
# github.com/google/go-flow-levee/guides/quickstart
./quickstart.go:14:13: a source has reached a sink
 source: ./quickstart.go:11:19
```

### テイント伝搬の確認
[quickstart.go](https://github.com/google/go-flow-levee/blob/v1.0.0/guides/quickstart/quickstart.go) を以下のように編集します。`Password` フィールドの情報を一度 `data` 変数に入れて、間接的に機密情報をログ出力するようにします。

```diff
-               log.Printf("unable to make authenticated request: incorrect authentication? %v", auth)
+               data := auth.Password
+               log.Printf("unable to make authenticated request: incorrect authentication? %v", data)
```

間接的な参照でも、正しく問題箇所が検出されました。:tada:

```
$ go vet -vettool=$(which levee) -config=$(realpath analyzer_configuration.yaml) ./...
# github.com/google/go-flow-levee/guides/quickstart
./quickstart.go:15:13: a source has reached a sink
 source: ./quickstart.go:11:19
```

以下のように、`data` 変数の代入を `Username` フィールドにすると、機密データがログに出力されなくなります。

```diff
-               data := auth.Password
+               data := auth.Username
```

問題がなくなったため、結果が表示されなくなりました。

```console
$ go vet -vettool=$(which levee) -config=$(realpath analyzer_configuration.yaml) ./...
# 何も表示されない
```

## Kubernetes での導入事例
Kubernetes での導入は [KEP-1933: Defend Against Logging Secrets via Static Analysis](https://github.com/kubernetes/enhancements/tree/master/keps/sig-security/1933-secret-logging-static-analysis) という KEP で提案されました。現在は、PR 時のチェック ([prow](https://github.com/kubernetes/test-infra/tree/master/prow)) の Job の一つ ([pull-kubernetes-verify-govet-levee](https://github.com/kubernetes/test-infra/blob/master/config/jobs/kubernetes/sig-instrumentation/verify-govet-levee.yaml)) として実行されています。

### Kubernetes の設定
Go Flow Levee の設定ファイルは [kubernetes/kubernetes](https://github.com/kubernetes/kubernetes) の [hack/testdata/levee/levee-config.yaml](https://github.com/kubernetes/kubernetes/blob/v1.23.2/hack/testdata/levee/levee-config.yaml) に置かれています。以下のような設定が記述されています。

- [ソース](https://github.com/kubernetes/kubernetes/blob/v1.23.2/hack/testdata/levee/levee-config.yaml#L26-L32)は主に `datapolicy` というフィールドタグで指定されている
    - このタグは [KEP-1753: Kubernetes system components logs sanitization](https://github.com/kubernetes/enhancements/tree/master/keps/sig-instrumentation/1753-logs-sanitization) で導入されたもの (この KEP 自体は deprecated)
    - 設定例: [Secret.Data](https://github.com/kubernetes/kubernetes/blob/v1.23.2/pkg/apis/core/types.go#L5180)、[TokenReviewSpec.Token](https://github.com/kubernetes/kubernetes/blob/v1.23.2/pkg/apis/authentication/types.go#L61)、[ExtenderTLSConfig.KeyData](https://github.com/kubernetes/kubernetes/blob/v1.23.2/pkg/scheduler/apis/config/types.go#L319)
    - 個別にソースを設定されているものもある
- [シンク](https://github.com/kubernetes/kubernetes/blob/v1.23.2/hack/testdata/levee/levee-config.yaml#L134)はロガーのパッケージを正規表現 `k?log` で対象としている

### チェックを試してみる
levee のチェックを試すため、Kubernetes の [pkg/kubelet/client/kubelet_client.go](https://github.com/kubernetes/kubernetes/blob/v1.23.2/pkg/kubelet/client/kubelet_client.go)に、以下のログを追加してみます。ログに記録している [KubeletClientConfig](https://github.com/kubernetes/kubernetes/blob/v1.23.2/pkg/kubelet/client/kubelet_client.go#L37) 構造体は BearerToken フィールドが[機密情報を含みます](https://github.com/kubernetes/kubernetes/blob/v1.23.2/hack/testdata/levee/levee-config.yaml#L92-L94)。

```diff
diff --git a/pkg/kubelet/client/kubelet_client.go b/pkg/kubelet/client/kubelet_client.go
index 20a4eb9df8b..d90eb82c7cc 100644
--- a/pkg/kubelet/client/kubelet_client.go
+++ b/pkg/kubelet/client/kubelet_client.go
@@ -30,6 +30,7 @@ import (
        "k8s.io/apiserver/pkg/server/egressselector"
        restclient "k8s.io/client-go/rest"
        "k8s.io/client-go/transport"
+       "k8s.io/klog/v2"
        nodeutil "k8s.io/kubernetes/pkg/util/node"
 )

@@ -173,6 +174,7 @@ type NodeConnectionInfoGetter struct {

 // NewNodeConnectionInfoGetter creates a new NodeConnectionInfoGetter.
 func NewNodeConnectionInfoGetter(nodes NodeGetter, config KubeletClientConfig) (ConnectionInfoGetter, error) {
+       klog.V(3).Infof("This log line leaks sensitive data: %v", config)
        transport, err := MakeTransport(&config)
        if err != nil {
                return nil, err
```

levee の実行スクリプト [hack/verify-govet-levee.sh](https://github.com/kubernetes/kubernetes/blob/v1.23.2/hack/verify-govet-levee.sh) を使って Go Flow Levee を実行します。今回追加した、問題のあるログが検出されました。:tada:

```
$ hack/verify-govet-levee.sh
# ... 省略

# k8s.io/kubernetes/pkg/kubelet/client
pkg/kubelet/client/kubelet_client.go:177:17: a source has reached a sink
 source: pkg/kubelet/client/kubelet_client.go:176:52
make: *** [vet] Error 1
```

## おわりに
誤って機密データをログに記録してしまう問題は、[CWE-532: Insertion of Sensitive Information into Log File](https://cwe.mitre.org/data/definitions/532.html) として共通脆弱性タイプ一覧にも分類されています。コードベースが大きくなるほど、こういった問題がないことを確認するのは難しくなります。不安があれば、一度 Go Flow Levee を試してみてはいかがでしょうか。