- 投稿日:2022-01-25T15:52:40+09:00
Go Flow Levee で機密データのロギングを検出する
はじめに セキュリティ上問題となるプログラミングエラーの一つに、誤って機密データをログに記録してしまうというものがあります 1。データ構造やロジックが複雑になるほど、レビューでもこの種のエラーは見落としやすくなります。Kubernetes でも何度かこの問題が起きており、Go Flow Levee というツールを使った静的解析による対策が導入されています。 本記事では、Go 言語用の静的解析ツール「Go Flow Levee」と、Kubernetes での導入事例についてご紹介します。 Go Flow Levee とは Go Flow Levee は、Go 言語用の静的解析ツールです。機密データなどの「ソース」が「サニタイザー」を通さずにログ関数などの「シンク」に到達していないかチェックできます。「ソース」をコピーした変数も「テイント」が付与されて伝搬が分析されることで、同様に「シンク」への到達を検出できます。2 認証情報などの機密データが誤ってログに記録されるようなケースを検出できるほか、ユーザからの入力に対してのテイントチェック(Taint Checking) としての使い方も可能です。 Go Flow Levee の基本的な使い方 Go Flow Levee の Quickstart のドキュメント をベースに、基本的な使い方をご紹介します。 検査するコード 以下の例 (quickstart.go) では、Authentication 構造体が Password というフィールドに機密データを保持しています。このコードでは log.Printf で、この構造体をログ出力しているため、機密データをログに記録してしまう問題があります。 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 という値が設定されていたときに機密データとして扱われます。 analyzer_configuration.yaml FieldTags: - Key: datapolicy Value: secret 機密データのフィールドにこのタグを設定します。 type Authentication struct { Username string Password string `datapolicy:"secret"` } フィールド名等で記述する 機密データを含むフィールド名、パッケージ名、型名を記述することもできます。外部パッケージなどタグを付与できない場合は、この方法を利用します。PackageRE, TypeRE, FieldRE を使って、正規表現を用いることもできます。 analyzer_configuration.yaml Sources: - Package: github.com/google/go-flow-levee/guides/quickstart Type: Authentication Field: Password この方法ではコードの変更は必要ありませんが、フィールド名などが変わった場合に忘れずこの設定も更新する必要があります。 シンクの設定 機密データが渡されてはいけない関数をシンク (Sink) として設定します。以下の例では log パッケージの Printf を対象としています。PackageRE、MethodRE を使うと正規表現を用いることもできます。 analyzer_configuration.yaml Sinks: - Package: log Method: Printf 検査を実行する まずは go-flow-levee (levee バイナリ)をインストールします。 go get github.com/google/go-flow-levee/cmd/levee 前述のサンプルコードが含まれる github.com/google/go-flow-levee/guides/quickstart を準備しておきます。 git clone git@github.com:google/go-flow-levee.git cd go-flow-levee/guides/quickstart go vet に levee バイナリを指定して実行します。設定は analyzer_configuration.yaml を参照してください。 go vet -vettool=$(which levee) -config=$(realpath analyzer_configuration.yaml) ./... 問題箇所が検出されました。 # 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 を以下のように編集します。Password フィールドの情報を一度 data 変数に入れて、間接的に機密情報をログ出力するようにします。 - 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) 間接的な参照でも、正しく問題箇所が検出されました。 $ 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 フィールドにすると、機密データがログに出力されなくなります。 - data := auth.Password + data := auth.Username 問題がなくなったため、結果が表示されなくなりました。 $ go vet -vettool=$(which levee) -config=$(realpath analyzer_configuration.yaml) ./... # 何も表示されない Kubernetes での導入事例 Kubernetes での導入は KEP-1933: Defend Against Logging Secrets via Static Analysis という KEP で提案されました。現在は、PR 時のチェック (prow) の Job の一つ (pull-kubernetes-verify-govet-levee) として実行されています。 Kubernetes の設定 Go Flow Levee の設定ファイルは kubernetes/kubernetes の hack/testdata/levee/levee-config.yaml に置かれています。以下のような設定が記述されています。 ソースは主に datapolicy というフィールドタグで指定されている このタグは KEP-1753: Kubernetes system components logs sanitization で導入されたもの (この KEP 自体は deprecated) 設定例: Secret.Data、TokenReviewSpec.Token、ExtenderTLSConfig.KeyData 個別にソースを設定されているものもある シンクはロガーのパッケージを正規表現 k?log で対象としている チェックを試してみる levee のチェックを試すため、Kubernetes の pkg/kubelet/client/kubelet_client.goに、以下のログを追加してみます。ログに記録している KubeletClientConfig 構造体は BearerToken フィールドが機密情報を含みます。 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 を使って Go Flow Levee を実行します。今回追加した、問題のあるログが検出されました。 $ 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 として共通脆弱性タイプ一覧にも分類されています。コードベースが大きくなるほど、こういった問題がないことを確認するのは難しくなります。不安があれば、一度 Go Flow Levee を試してみてはいかがでしょうか。 共通脆弱性タイプ一覧 CWE では、この問題を CWE-532: Insertion of Sensitive Information into Log File として分類しています。 ↩ Taint propagation analysis (テイント伝搬解析) と呼ばれる手法です。「テイント (taint)」「シンク (sink)」などの用語もこの分野の専門用語になります。 ↩
- 投稿日:2022-01-25T13:29:21+09:00
【GoでMockを差し込めるようにリファクタリングした】
現在Goで作っているアプリケーションを先輩からアドバイスをもらって、より疎結合に実装できるようになったのでまとめる。(MVCの概念の中で行うことを前提にしています) 何をMock化したいのか? 今回のリファクタリングを通じて思ったのは、「どこの関数をmock化したいのか」をまず考えるべきと言うこと。 今回の例だとcontrollerのuser.setvalue()をmock化できればCreateUserの単体テストが楽になりそうだ。 userController.go package controllers import ( "app/~/models" "github.com/gin-gonic/gin" ) func CreateUser(c *gin.Context) { user := &models.User{} c.BindJSON(user) user.SetValue(*user) /* <--------- ここをMock化できたらよさそう*/ c.JSON(200, gin.H{ "result": true, }) } どうやってやるのか? Model側の設定 このuser.setvalue()はUserModelの関数であるため、UserModelが実装となるinterfaceを定義してあげる必要がある。 userModel.goとuserInterface.go package models type User struct { Name string `json:"name"` Email string `json:"email"` } func (u *User) SetValue(user User) { // 具体的な処理 } package models type UserInterface interface { SetValue(user User) } これで、userModelの代わりとなるをmockを差し込むためのModel側の準備は完了。 controller側の修正 あとは、mockを使うのか実際のmodelを使うのかをインスタンスを生成するタイミングで差し込んであげればいい(コンストラクタインジェクションともいう。)のでそのための修正をcontrollerに加える。 オブジェクト指向言語であれば、コンストラクタがあるのでそこの引数とインスタンス変数で実現できる。しかし、Goにはこういったものはないので独自にNew関数とインスタンス変数っぽいものを作る。New+〜とするのが推奨されているらしい。 修正後 userController.go package controllers import ( "app/~/models" "github.com/gin-gonic/gin" ) type UserRepository struct { User models.UserInterface /* <-- インスタンス変数っぽく扱うもの */ } /* コンストラクタの代わりになるもの */ func NewUserRepository(user models.UserInterface) *UserRepository { return &UserRepository{ user, } } func (r *UserRepository) CreateUser(c *gin.Context) { user := &models.User{} c.BindJSON(user) r.user.SetValue(*user) /* <--------- UserRepositoryのUserのSetValueを使用する*/ c.JSON(200, gin.H{ "result": true, }) } 実際にNewするところ これでようやくインスタンス生成時に、インターフェースを介して疎結合にモデルかmockを差し込める。 controllerに処理を渡す上位層でNewすればいい。 &models.User{}をMockにすれば、CreateUser()の単体テストは楽になるだろう¥ userCnt := controllers.NewUserRepository(&models.User{}) userCnt.CreateUser() まとめ Goで疎結合な設計を行うためには、構造体とinterfaceをうまく使う必要がある。
- 投稿日:2022-01-25T02:33:35+09:00
entでUpsertを実装する(for PostgreSQL, SQLite3)
はじめに 仕事でORMにentを使いAPIを作成しているんですが、Upsertを実装する際にPostgresだとつまずいた部分があったため、Tipsになればと良いなと思いやったことを書き残していきます 発生した問題 Upsertを実装したものの SQLiteでは正常に動作する PostgreSQLでは下の様なエラーが発生する CreatedAtは更新したくないのに更新されてしまう ERROR: ON CONFLICT DO UPDATE requires inference specification or constraint name (SQLSTATE 42601) 今、こんな問題が起きてる方は 5. から見ると良いと思います 5. Upsertを実装する場合 目次 ent v0.9.0からUpsertが利用できる様になりました generate.goにsql/upsert機能フラグを追加します Create(データをinsert)する場合 特定のやつをUpdateする場合 Upsertを実装する場合 まとめ 参考 ent v0.9.0からUpsertが利用できる様になりました ↑ この記事を参考に書いていけばSQLite3の場合は期待通りUpsertの実装を行うことが可能です 下記にUpsertを実装する手順を書いていきます generate.goに sql/upsert 機能フラグを追加します /ent/generate.go package ent - //go:generate go run -mod=mod entgo.io/ent/cmd/ent generate ./schema + //go:generate go run -mod=mod entgo.io/ent/cmd/ent generate --feature sql/upsert ./schema # generate.go に sql/upsert を追加したら再度、schemaをgenerateしましょう # 再generate すると `OnConflict` という関数が追加されます $ go generate ./... Create(データをinsert)する場合 // client: *ent.Client // User: *ent.UserClient ← schema.go の内容によって User の部分は名前が変わります // ↑ この2つは schema.go の内容によって自動に生成されます u, err := client.User. Create(). SetUserID("A0001") SetName("user1"). SetAge(20). Save(ctx) ent docs: エンティティを作成する 特定のやつをUpdateする場合 u, err := client.User. UpdateOneID(id). SetUserID("A0001") SetName("user1"). SetAge(24). Save(ctx) // or u, err := client.User. Update(). Where(user.UserID("A0001")). SetName("user1") SetAge(24). Save(ctx) ent docs: ID指定で更新する Upsertを実装する場合 この書き方はPostgresでは動作しません(SQLiteでは動作します) conflictが起きた際はUpdateを行うといった処理の書き方になります id, err := client.User. Create(). SetUserID("A0001") // UserIDがUnique指定されている場合 SetAge(24). SetName("user1"). OnConflict(). // 何かしらのconflictが発生したらUpdateを行う UpdateNewValues(). ID(ctx) Postgresで動作する様に修正します Postgresの場合はconflictが起きる可能性のあるカラムを事前に明記しておく必要がありますOnConflictの引数に指定してあげましょう ただ、この書き方ではdefaultで作成されるcreated_atやupdated_atの両方とも更新されてしまいます id, err := client.User. Create(). SetUserID("A0001") // UserIDがUnique指定されている場合 SetName("user1"). SetAge(24). - OnConflict(). + OnConflict( + sql.ConflictColumns(user.FieldUserID), + ). UpdateNewValues(). ID(ctx) Insertされる場合はcreated_atが作成されて、Updateされる場合はcreated_atが更新されない様にします id, err := client.User. Create(). SetUserID("A0001") SetName("user1"). SetAge(24). OnConflict( sql.ConflictColumns(user.FieldUserID), ). - UpdateNewValues(). + Update(func(u *ent.UserUpsert) { + u.SetUserID("A0001") + u.SetName("user1") + u.SetAge("24") + u.UpdateUpdatedAt() + }). ID(ctx) ent docs: 1つをUpsert まとめ entを使ってUpsertを実装する場合、conflictが発生するカラムはOnConflictの中で指定した方がいいでしょう SQLiteやPostgresなど複数の環境で同じコードを使えます OnConflict( sql.ConflictColumns(user.FieldUserID), ). Updateしたくないカラムがあれば、Updateしたいカラムだけ指定しましょう Update(func(u *ent.UserUpsert) { u.SetUserID("A0001") u.SetName("user1") u.SetAge("24") u.UpdateUpdatedAt() }). 参考 バージョン0.9.0でUpsert APIを追加しました! CRUD API - ent.io