Goセキュリティチートシート:Go開発者のための8つのセキュリティベストプラクティス
Gerred Dillon
2021年2月9日
0 分で読めますチートシートシリーズの今回は、Go開発者のための8つのセキュリティベストプラクティスを紹介します。Go言語には、メモリのガベージコレクションや型安全性の高いポインターなど、Cのような古くて低レベルな言語と比べて、より安全な開発を促す多くの機能が組み込まれています。
こうした機能により、メモリを自分で管理する必要がなくなり、脆弱性につながるバグを避けやすくなります。それでも、プログラマーが把握しておくべきセキュリティのベストプラクティスは存在します。Eric Smalling、Gerred Dillonが執筆し、SnykのシニアソフトウェアエンジニアであるDan Enmanの協力を得て作成したこのチートシートでは、よくあるトピックをいくつか取り上げます。
Go Modulesを使う
依存関係をスキャンしてCVEを検出する
Go標準の暗号パッケージを使う
html/templateを使ってXSS攻撃を防ぐ
サブシェルの使用
unsafeとcgoを避ける
リフレクションの使用は最小限にする
コンテナの攻撃対象領域を最小化する
1. Go Modulesを使う
Go Modulesは、v1.11以降の公式依存関係管理システムです。従来のVendorとDepは非推奨になっています。Go Modulesでは、推移的なモジュールを含めて依存関係のバージョンを固定できます。また、go.sumチェックサムデータベースにより、予期しないモジュールの変更を防ぐことができます。
まず、最上位のディレクトリでgo mod init [namespace/project-name]を実行し、プロジェクトを初期化します。
これにより、現在のディレクトリにgo.modというファイルが作成され、プロジェクト名と現在使用しているGoのバージョンが記録されます。ソースコードにパッケージのインポートがある場合、go build(またはtest、installなど)を実行するだけで、使用中のモジュールとそのバージョンがgo.modに反映されます。go getを使って依存関係を更新することもできます。特定のバージョンへの更新も可能で、その内容もgo.modに反映されます。
go.modファイルの例:
go.sumというファイルも作成されていることに注目してください。このファイルには、使用する各モジュールのハッシュが記録されており、Goはこれを利用して、すべてのビルドで同じバイナリが使われることを検証します。go.modとgo.sumの両方を、アプリケーションコードとともにソース管理にチェックインしてください。
Go公式ブログのチュートリアルUsing Go Modulesは、Go Modulesについてさらに学ぶのに役立つ優れた資料です。推移的な依存関係のバージョン固定や、使われていない依存関係の整理などを解説しています。
2. 依存関係をスキャンしてCVEを検出する
多くのプロジェクトでは、アプリケーション自体よりも、依存先のモジュールに含まれるコードのほうが多くを占めます。こうした外部依存関係は、セキュリティの脆弱性が入り込む一般的な経路です。広範な脆弱性データベースを活用するSnykなどのツールを使えば、依存関係のグラフをスキャンして既知の脆弱性を検出し、修正に役立つアップグレードを提案できます。さらに、今後新たに発見される脆弱性について、プロジェクトを継続的に監視することも可能です。
たとえば、Goアプリケーションでsnyk testを実行するだけで、モジュールが解析され、既知のCVEと、アップグレード先となる修正版の情報が報告されます。また、SnykのWebベースのツールでは、GitHubリポジトリを直接継続的に監視できます。コードを変更していない場合やCIビルドを実行していない場合でも、今後発見された脆弱性について通知を受け取れます。


3. サードパーティ製ではなくGo標準の暗号パッケージを使う
Go標準ライブラリのcryptoパッケージは、セキュリティ研究者による十分な監査を受けています。しかし、すべての機能を網羅しているわけではないため、サードパーティ製のパッケージを使いたくなることもあるでしょう。
暗号アルゴリズムを自作しないのと同様に、サードパーティ製の暗号ライブラリにも十分注意してください。同じレベルの厳密な監査を受けているとは限りません。提供元を確認しましょう。
4. html/templateを使ってXSS攻撃を防ぐ
io.WriteString()またはtext/templateパッケージを使って、フィルタリングされていない文字列をWebクライアントに返すと、ユーザーがクロスサイトスクリプティング(XSS)攻撃にさらされる可能性があります。返される文字列内のHTMLタグがエンコードされずに出力されるうえ、明示的に設定しないとContent-Type: plain/textレスポンスヘッダーが誤って設定される可能性があるためです。
html/templateパッケージを使えば、アプリケーションのロジックで手作業によるエンコードを徹底する代わりに、返すコンテンツを自動的にWeb向けにエンコードできます。このトピックについて、詳しい解説と例を掲載したOWASP/GO-SCPのドキュメントも参考になります。
5. サブシェルの使用
Goでのsubshellは、基本的にシステムへの直接的なシェルアクセスを可能にするもので、通常はコマンドラインツール型のアプリケーションでのみ使われます。可能な限り、適切なモジュールを使い、Goコードでネイティブに実装する方法を優先してください。
サブシェルを使う必要がある場合は、外部から取得したデータを渡す前に適切にサニタイズしてください。また、返されるデータもサニタイズし、アプリケーションが基盤システムの不要な情報を漏らさないようにしましょう。これは、レンダリングされたテンプレートへの攻撃(上記の#4を参照)やSQLインジェクションへの対策と同様です。アプリケーションのリクエスト処理中に外部プロセスを実行すると、Goコードから制御できない副作用が起きる可能性も考慮してください。たとえば、ファイルシステムの変更、外部依存先への呼び出し、コンテナの実行制限やAppArmor、SELinuxなどのツールによるセキュリティ環境の変更や、呼び出しのブロックなどが挙げられます。
6. unsafeとcgoの使用には注意する
C言語と同様、Goでもポインター型の変数を使えますが、意図しない、あるいは悪意のある副作用から開発者を守るため、厳格な型安全性が設けられています。Cでは型が割り当てられていないvoid*ポインターを定義できます。Goで同様のことをするには、型安全性の制約を破るunsafe標準パッケージを使います。unsafeはメモリに直接アクセスできるため、一般にGoのドキュメントでも使用が推奨されていません。ユーザーデータと組み合わせると、攻撃者がGoのメモリ安全性を破る可能性があります。
同様に注意が必要なのがcgoです。Goアプリケーションに任意のCライブラリを統合できる強力なコマンドですが、強力なツールと同様、cgoは細心の注意を払って使う必要があります。安全でない言語で書かれた外部依存先が、すべてを正しく処理していると信頼することになるためです。外部コードにバグや悪意ある処理が潜んでいても、Goのメモリ安全機能では保護できません。cgoは、ビルド時にCGO_ENABLED=0を設定するだけで無効にできます。明示的に必要でない場合は、通常これが安全な選択肢です。最近のGoライブラリの多くは純粋なGoコードで書かれています。
7. リフレクション
Goは静的型付けの言語であり、変数の型は重要です。実行時に、変数の型や値に関する情報をコード内で参照する必要が生じることもあります。Goには、任意の型を持つ変数の型や値を調べたり操作したりできる`reflect`パッケージがあります。たとえば、変数が特定の型かどうか、または特定のプロパティや関数を持つかどうかを確認できます。
リフレクションは便利な一方で、Goコードの実行時型エラーのリスクを高めます。リフレクションで取得した変数を、許可されていない方法で変更しようとすると(たとえば、構造体の設定できない値を設定しようとすると)、コードはパニックを起こします。また、コードの流れや、参照されるさまざまな型の種類、値の種類を把握するのが難しくなる場合があります。さらに、リフレクションで取得した型や値を扱う際には、型アサーションが必要になることがあります。コードが分かりにくくなり、実行時エラーにつながる可能性もあります。
リフレクションは強力なツールですが、Goの型システムとインターフェースの仕組みを踏まえると、予期しない問題を簡単に引き起こす可能性があるため、使用は最小限にとどめるべきです。
8. コンテナの攻撃対象領域を最小化する
Goアプリケーションの多くは外部依存関係を持たず、コンテナで実行されるよう設計されています。そのため、いくつかのイメージ構築手法を使い、利用可能なファイルシステムを最小限にしましょう。簡単な方法の一つは、マルチステージDockerfileを使ってビルドステージでアプリケーションをビルドし、デプロイ用イメージにはscratchベースイメージを使うことです。
次のDockerfileの例を見てみましょう。
Dockerfileに馴染みがない方のために説明すると、DockerfileはほぼすべてのOCIイメージビルドでイメージを構築するために使える手順書です。詳しくはこちらをご覧ください。この例はマルチステージDockerfileで、ビルドステージと最終的なランタイムイメージステージの2つに分かれています。
ステージ1、1~12行目:ビルドステージ
公式のgolang:1.15ベースイメージから開始し、このステージで環境変数を設定してGoアプリケーションをビルドします。このステージが完了すると、buildというラベルの付いた一時イメージがキャッシュされ、後で参照できるようになります。
ビルドに渡している環境変数や引数の意味が気になるかもしれません。
GOPATH=””:この変数(golang:1.15ベースイメージで設定済み)をクリアします。Go Modulesを使う場合は不要です。CGO_ENABLED=0:cgoを無効にします(上記セクション6を参照)。GOOS=linux:Linuxオペレーティングシステム向けにビルドすることを明示します。GOARCH=amd66:amd64(Intel)アーキテクチャ向けにビルドすることを明示します。-trimpath:バイナリからファイルシステムのパス情報を削除します。ldflag -s:シンボルテーブルとデバッグ情報を省略します。ldflag -w:DWARFシンボルテーブルを省略します。
これらの設定を組み合わせると、ほぼ最小限のバイナリファイルをビルドできます。ただし、アプリケーションによっては適さない設定もあるため、必要に応じて選択してください。
ステージ2、13~10行目:ランタイムイメージステージ
このステージでは、静的バイナリと/etc/passwdファイルをbuildステージから空のscratchファイルシステムにコピーし、適切な所有権とコンテナ起動時に実行するコマンドを指定します。
このイメージをビルドするには、Dockerfileと同じディレクトリで次を実行します。
注:ビルド行の最後にある.は重要です。ビルドシステムに、Dockerfileと参照されるその他のファイルの場所を伝えます。
生成されるイメージのファイルシステムには、アプリケーションとmobyユーザーを記載したpasswdファイルの2つだけが含まれます(ユーザー名は重要ではありません。コンテナ内でrootとして実行したくないためです)。攻撃者に悪用される可能性のあるsh、psなどのファイルは含まれません。もちろん、アプリケーションの動作に他のファイルが必要な場合は、それらを含めるか、実行時にマウントしてください。
Capture the Flagを始めよう
オンデマンドのバーチャル入門ワークショップを見て、Capture the Flagの課題の解き方を学びましょう。
