日々コードを書く中で起こり得るインセキュアなコード

米内貴志氏:米内です。今日は「Goをセキュアに書き進めるための『ガードレール』を整備しよう」という題で、組織としてGoを使うときにセキュリティを底上げするための仕組み作りについて話をしようと思います。よろしくお願いします。

まず簡単に自己紹介をさせてください。私は米内貴志と申します。株式会社Flatt Securityでセキュリティプロダクトの開発、今は特にeラーニング事業の開発をしています。社内でもけっこうGoを使っています。

もともとWebブラウザーとセキュリティが好きで、最近『Webブラウザセキュリティ』という本をラムダノートさんから出版しました。気になる方はぜひ読んでみてください。

ここから本題に入っていきます。Goに限らず日々コードを書いていく中で、セキュリティ的に良くないコードを書いてしまうことはけっこうあると思うんですね。知ってか知らずか書いてしまうときがあると思うんです。

例えばちょっと低いレイヤーの操作でunsafeなコードを書いてしまって、それが意図せぬ脆弱性になってしまったり、L7の話で他のサーバーと通信をするときに証明書検証が邪魔だったから一時的に切って、証明書検証を切ったままコードを出荷してしまったり。

「Goの乱数生成のパッケージは、セキュリティ用途ではあまり使わないほうがいいよ」「crypto/randなどのパッケージを使ってね」とドキュメントに推奨が書いてあるんですが、それを知らずにセキュリティ的に良い乱数が欲しいときにmath/randパッケージを使ってしまうこともしばしばあるわけです。

メモリセーフでうれしいんですが、Goを使っているからといって、何もかも安全になるというわけではないという点には注意しないといけません。そういった問題を減らして、組織のGoのコードをセキュアに保っていきたいと考えたときに、ここから話す3つの課題にぶち当たると思います。

「Go」のコードをセキュアに保つために解決すべき3つの課題

まず1つ目。「そもそも何を気をつけなければいいのかわからない問題」ですね。これは大きな課題です。先ほどの乱数生成の話もそうですが、いちいちドキュメントのすべてを読んで、「こうすべき」をつぶさに確認してからコードを書いているわけではないと思うので、知らず知らずのうちに、クリティカルではないものの、あまり安全ではないコードを書いてしまうことがあるんですね。

知らず知らずのうちにというのがポイントで、「そもそも何に気をつければいいのかがわからない」というのが課題になることがよくあると思います。

2つ目です。自分自身は気をつけていても、その知識がチームに浸透しにくいというのも組織がコードのセキュリティを高めていく上では1つ課題になると思います。コードレビューのときに「これはこっちを使ったほうがいいよ」などと伝えることはできますが、どうしても人伝になってしまうので、組織にスケールしないのが課題です。

3つ目の課題ですね。先ほど、一時的に証明書検証をオフにしたままコードを書いて、そのあとに証明書検証をオンにするのを忘れて出荷するミスもあると話したと思うんですが、知っていても注意不足で脆弱なコードを書いてしまうことは十分にあります。セキュリティエンジニアでさえも、インセキュアなコードを書いてしまうことがあるわけですね。

これら3つの課題、「そもそも気をつけるべきところがわからない」「自分はわかっているけれどチームにその知見が浸透していかない」「知っていても注意不足で脆弱なコードを書いてしまう」。これらを解決していくための何か良い仕組みが欲しいわけです。

まず1つ目によくあるミスについてです。全世界いろいろなところでGoのコードは書かれていますが、多くの人がやるミスのベースをうまく拾ってくれるLintみたいな仕組みが欲しいです。また、知識がスケールしないという課題を解決するための仕組み作りもしていきたいです。

よくあるミスを拾ったり、組織固有のミスを検出しやすくしたりするのは、できるだけCIの中に組み込むとして、自分が注意をし続けなくてもいいようにしていきたい。

そんな仕組みを作っていきたいわけで、ここからは組織的にセキュリティをうまくやっていくための、頭を使わなくても道を踏み外さないための仕組みを「ガードレール」と呼びます。この言葉はけっこういろいろなクラウドベンダーが使っています。

「ガードレール」の仕組みを作るための3つのツール

ガードレールみたいな仕組みを実際に作っていくための3つのアクションについてお話ししたいと思います。まず1つ目のアクションが「gosec」の導入です。これはそもそも何に気をつければいいのかわからないという課題を解決するためのアプローチとして紹介します。

その次にお話しするのは、静的解析ツール作りを助けてくれる「go-ruleguard」というツールです。これは、自分はセキュリティや社内のパッケージに関する注意点がわかっているけれど、その知見がうまく浸透していないときに使えるものです。

ガードレールを作るための材料を説明したあとは、運用に持っていくときの課題です。False Positiveなアラートが出てしまいがちというところに着目をして、例えば導入の初期には「reviewdog」みたいなツールを使ったらいいよねとか、運用のときに実際にここから始めるといいんじゃないかなという話をできればいいなと考えています。ここからはこの順番で、今取れる3つのアクションについてご紹介します。

典型的インセキュアコードパターンを検出する「gosec」

まず、何に気をつければいいのかがわからないというところを解決するためのアプローチとして、gosecを紹介したいと思います。gosecを一言で言うと、典型的なインセキュアなコードパターンを検出してくれるツールです。

スライドの下のほうに、実際にgosecを実行したときの例が出ているんですが、例えばcrypto/randの代わりにmath/randを使うと、乱数の性質としてはセキュリティに良くないよとG404という名前のルールが発見してくれます。

具体的に、弱い乱数生成器が使われていること、それが実際にどこで使われているか、そしてそれがよくある脆弱性の累計であるCWE(Common Weakness Enumeration)の中でどのパターンに当てはまる脆弱性かを出力してくれます。

実行のときにgosec ./main.goと叩いていますが、これだけで実行できるけっこう簡単なツールで、例えばGoで書かれているKubernetesやCaddyなどでも使われているツールです。

話の始めに「こんなミスが時々あるよね」とか「気をつけていないとこんなことやっちゃうよね」という話をしたんですが、その中で出てきたunsafeだったり、証明書の検証の無効化だったりはそれぞれG103、G402、G404みたいにgosecが持っているルールで自動検証できます。

gosecは、Lintツールなどの他の静的解析ツール同じくgo/astやgo/typesみたいなもので作られています。例えばさっきのInsecureSkipVerify、TLS(Transport Layer Security)検証を無効化するTLSConfigのオプションを無効化しているものを探すコードであれば、スライドの下に出ているコードですね。

標準ライブラリを使う上で、注意しないといけないとわかっている脆弱的なコードパターンがあれば、astのパターンを書いて、gosecにコントリビュートするプロセスで、その知見を組織や全世界に対してコントリビュートできます。

コードパターンを検出して修正案を提案してくれる「go-ruleguard」

2つ目の課題です。自分の知見を組織にどうスケールさせていくかの話をしていきます。静的解析ツールを作れる知識があれば、gosecのルールを足して、gosecを拡張して、実際にその知見をスケールできるかたちにするのも可能ではあるんですが、会社がけっこういろいろな社内パッケージや社内ライブラリを持っているんですよね。

そのプロダクトのドメインでしか使われないパッケージをけっこう持っているんですが、OSSなので、そういうものに対して例えばこういうコードパターンは危ないよねというセキュリティルールをgosecの中に放り込むのは難しいんですよ。

スライドの下のほうに「そこそこ危ない関数」と書いてある関数がありますが、これは処理の共通化のために関数化していて、ユーザー入力を受けて生成された値を引数として取りたくないなと思いながら書いたとします。

スライドの下のほうに関数呼び出しが3つぐらいあります。上の2つのような、コードを書いているときにコンパイル自身が決定できる定数でこの関数呼び出しがあるときは別に構わないけれど、最後の、動的に変わりうる変数を使ってその関数が呼び出されたときには「それ大丈夫か?」とアラートを出したいというケースがあるとします。

気持ち的にはgosecみたいなかたちで、自分がコード化して、他の人が似たようなコードを書いたときにアラートを出せるようにしたいのですが、gosecにコントリビュートするのは社内ライブラリや社内パッケージだとちょっとキツイし、かと言って自分でgo/astやgo/analysisみたいなパッケージを触って書いていくのはそこそこメンテナンスコストもかかります。

そういった意味で、組織固有の良くないコードパターン、避けてほしいコードパターンをコード化してスケールできる状態にするのはここまで紹介した方法だけだと、まだ課題が残る状態です。そこで今回紹介したいのがgo-ruleguardです。これを一言で表すと、Goコード向けの独自の静的解析ルールです。さっきみたいなコードを検出するルールと、それを修正の提案があったらアラートを行って補助するLinterです。

具体例を見たいと思います。スライドの中腹に、セキュリティと関係ない話ですが、キャストが走ってしまうcopy関数の呼び出しがあったとします。実際にキャストを走らせる必要はないので、コードパターンを検出して、修正を提案を受けたいとします。そんなときに、go-ruleguardは、スライドの一番下にある3行みたいなかたちで、検出と修正例の提案をしてくれます。

一番下の3行の1行目はMatchでコードパターンです。copy関数の呼び出しで、こんな引数があるというのをパターンマッチ的に記述しています。具体的には$bや$sがパターンです。その次の行のWhereで、1行目のMatchで引っかかるコードパターンで、特にこういう条件を満たすものと絞り込みます。

具体的には、byteのキャストの引数の中にString型の値が埋まっている場合に絞り込みます。コードが見つかったら、最後の3行目のSuggest('copy($b, $s)')でbyteのキャストを取り除いたかたちに修正するのを提案します。

もともとの記法自体は、gogrepやgo/analysisなどの仕組みから来ているんですが、たった3行で検出したいパターンと、それをどう修正すべきかを提案できるのがgo-ruleguardです。

社内ライブラリや社内パッケージに対するコード検出は、ちょっと難しいよねと先ほど話したんですが、go-ruleguardを使うと、それも比較的容易にできます。

例えば、コードの中で危ない関数の呼び出しがあったとして、その引数がコンパイル時に定数でないものを検出したいときは、真ん中に書いてあるたった3行のルールで検出してReportすることができます。これを噛み砕くと、関数の呼び出しで引数が$x、プレイスホルダー xであるもので、かつxがコンパイル時に定数でないときに、最後の行でReportします。コンパイル時に定数じゃないよとレポートするのはたったの3行ですね。

これで検出のルールを書けます。実際にこれを走らせてみると、上の2行ですね。コンパイル時に値が決定するものに対してはアラートが出ずに、コンパイル時に値が決定しないものに対してはアラートを出す。そんなことができます。

Policy as Codeをコードに対して行うアプローチとして、他のツールもあります。例えば「semgrep」はその1つの例です。これはGo以外に対しても使えるツールです。

もうちょっとパワフルな機能を持ったもので、GitHubの「CodeQL」も同じようなことに使えるツールです。これらと比較してgo-ruleguardにどういう利点があるかというと、やっぱりGoに特化しているというところです。

semgrepとかCodeQLとか、いろいろなプログラミング言語に対して適用できるツールだと、それこそコードの検出時の記法がちょっと冗長になってしまったり、細かいところに手が届かなかったり、特定の事象に手が届かなかったりしやすいんですが、go-ruleguardは比較的そういった制約がなくて、例えば先ほど言ったようなコンパイル時の定数の取り扱い方もけっこう上手にやることができます。

また、型情報を用いたものですね。スライドの右下に出ているコードサンプルのような型情報を用いた条件の絞り込みは、パターンマッチが基本的に便利です。Goの世界であえてgo-ruleguardを使う利点なんじゃないかなと思っています。補足ですが、このツールはけっこうサポートも厚くて、チュートリアルもあります。

Ruleguard by exampleを見ていくと、具体的にどんなふうに静的解析のルールを書けて、どんな制約が付けられるのかがけっこう細かく書いてあるので、ぜひこちらも見てもらうといいんじゃないかなと思います。ここまでが2つ目の話ですね。

差分があったところだけコメントができる「reviewdog」

ここからは、具体的にこれを運用に回すときの話をしていきたいと思います。セキュリティに関するツール、セキュリティに関する仕組みを作っていく上で、アラートが大量に出るという課題があります。例えば今回の話を聞いて「おもしろそうだな」「使ってみるか」と思って、READMEを読んで、Gitのcurlコマンドを貼ってgosecコマンドを叩いたときに、自分のプロジェクトから543個のエラーが出たとします。

これは543個の問題があるというわけではなくて、あくまでも問題の可能性のあるポイントが出てきているんですね。例えば、セキュリティ的に良い性質が求められない乱数が欲しいときには、crypto/randよりもmath/randパッケージを使ったほうがパフォーマンス的にもいいとか、ここは別に直さなくてもいいとか、いちいち543個に対して頭を使ってどう直すべきかを考えないといけないです。こうなるともう大炎上ですよね。

しかも、継続的に改善していくためにCIにこのツールを入れようとしても、実際にCIが通る状態にするためにはその543個に対して、これは1回ルールを無効化するかとか、これは直すとか判断しなきゃいけないので、導入の障壁が高くなっちゃいます。

とはいえ、そのまま入れたらずっとCI自体が落ちた状態になっちゃうので、それだとコードにわざわざこういったものを入れる意味もなくなってしまいます。

そういったところで今回最後に紹介するのがreviewdogというツールです。これはけっこう昔からあるんですが、一言で言うと各種のLinterのツールの出力です。gosecが出したアラートなどをGitHubなどでPR(Pull Request)のコメントとして投稿できるツールです。

これのおもしろいところは、差分に対してのみコメントを付けてくれるという点です。例えばさっき、いろいろなFalse Positiveで543個という数字がありましたが、それがたくさん出たとしてもいったんその部分は置いておいて、実際にコードの差分が発生したときにその差分に対してのみコメントできます。

具体的にはこんな感じですね。GitHubの場合ですが、PR(Pull Request)を出したときにgithub-actionsの中でこういうgosecを走らせて、その結果をreviewdogに流してあげると、差分があったところにだけ「こんなアラートが来ているけど大丈夫?」みたいなコメントを付けられます。

コードの初期に大量のアラートが出てしまう環境でも、段階的に改善を進めていけるのがうれしい点です。特にセキュリティツールは確証を持って「これは脆弱です」と言えている場合が少ないんですが、そういったものであっても比較的導入しやすくなるというのが、セキュリティツールと併用するひとつのメリットだと思います。

今回この発表では、組織のGoコードをセキュアにする上での3つの課題を紹介しました。それに対して今回は、3つのアプローチ方法を紹介しました。

1つ目がgosecで、「全体的なよくある問題をフォローする」です。2つ目がgo-ruleguardで、「よくある問題だけじゃなくて組織固有の問題もコード化できるようにする」です。最後に紹介したのがreviewdogで、「False Positiveを多めに検出しても併用できるようにする」です。この3つを紹介しました。

ガードレールを整備して、安心・安全なGoライフを送ってもらえれば幸いです。私からの発表は以上です。ご清聴ありがとうございました。