Flatt Security Blog

株式会社Flatt Securityの公式ブログです。プロダクト開発やプロダクトセキュリティに関する技術的な知見・トレンドを伝える記事を発信しています。

株式会社Flatt Securityの公式ブログです。
プロダクト開発やプロダクトセキュリティに関する技術的な知見・トレンドを伝える記事を発信しています。

セキュアにGoを書くための「ガードレール」を置こう - 安全なGoプロダクト開発に向けた持続可能なアプローチ

f:id:flattsecurity:20210223151548p:plain

The Go gopher was designed by Renee French. (http://reneefrench.blogspot.com/) The design is licensed under the Creative Commons 3.0 Attributions license.

種々の linter が様々なプロダクトの品質を高めてきた、というのは疑う余地のない事実です。実装の初歩的な問題をエディタ内や CI/CD パイプライン中で機械的に検出できる環境を作れば、開発者はコーディングやコードレビューの邪魔になる些末な問題を早期に頭から追い出し、本質的な問題に集中できます。

また、そのような環境づくり(e.g. linter のルールセットの定義、組織独自のルールの作成、…)は、まさに開発組織のベースラインを定義する作業として捉えることができます。一度誰かが定義した linter のルールは、開発組織全体のコードの品質を継続的に高めてくれることでしょう。大抵のプロダクト開発は一人だけでは達成できませんから、このような「組織にスケールする持続可能な開発品質向上のためのアプローチ」は多くの開発組織にとって重要なものです。

ところで、実装のセキュリティに関しても、同じようなアプローチが取れないものでしょうか。先述の linter の話になぞらえて表現するなら、いわば開発組織のガードレールとも呼べるものが定義できないものでしょうか1。もし実装のセキュリティ上の問題のうち些末なものを早期に・機械的に検出でき、かつ組織にスケールさせられるような方法があったとしたら、開発者は本質的な問題に集中できるはずです。プロダクトのセキュリティも底上げできることでしょう。

そこで本稿では、そんな疑問に答えるべく、Go 言語で書かれたコードを対象にセキュリティポリシーを作成・適用・運用する方法を紹介します。

「ガードレール」の構成

ここからは Go 言語によるプロダクト開発のためのガードレールとして、実際に GitHub と GitHub Actions を用いて作成した、以下の少なくとも一方を満たすような環境の例をいくつか示そうと思います2

  • 要件1: Go コードのセキュリティに関する些末な問題を機械的に検出・報告してくれる
  • 要件2: セキュリティに関するルールを後から自分で追加できる
    • ルールの例: 「危ないと分かっている関数 f の呼び出しを原則禁止する」
    • ルールの例: 「昔脆弱性診断で問題が報告されたコードと同じコードパターンを禁止する」

具体的には以下の 3 つのアプローチを紹介します。

もっとも、既存のコードに後から linter やそれに類する仕組みを入れると、一向に通らない CI が出来上がることが多々あります。そうなると人はいつしか CI を無視し始めたり、重要度の高い lint ルールを無効化し始めたりするものです。このような「死んだ CI」が出来上がってしまっては本末転倒です。

そこで本稿では reviewdog というツールを用いて、GitHub 上での Pull Request により生じた差分のみを対象にしてセキュリティ上の問題を検出・報告してもらう ことにします。これにより既存コードへのアラートによるストレスを軽減しつつ、コードを漸進的に改善していくことができるはずです。

gosec - セキュリティに特化した簡易的なチェック

自組織でカスタムルールを定義できるような機能(要件2)が必要なく、簡単な項目のチェック機能(要件1)があれば十分だという場合には gosec が利用できます。gosec は go/astgo/types ベース3の Go コード向けセキュリティスキャナであり、比較的導入障壁が低いのが特徴的です。

ここからは 実際に gosec を設定した GitHub 上のリポジトリ例(security-aware-repo-examples/gosec)を用いて、reviewdog や gosec の動作を確認してみましょう。

まず、このリポジトリでは以下のような GitHub Actions の Workflow を定義しています。この Workflow は Pull Request の発行時に reviewdoggolangci-lint を呼び出すというものです。

name: Run security tests

on:
  pull_request:

jobs:
  security:
    runs-on: ubuntu-latest
    env:
      GO111MODULE: on
    steps:
      - name: Checkout sources
        uses: actions/checkout@v2
      - name: Run golangci-lint
        uses: reviewdog/action-golangci-lint@v1
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          level: warning
          golangci_lint_flags: "--config=.golangci.yml"
          filter_mode: nofilter
          reporter: github-pr-review

このうち reviewdog は先述の通り、各種 linter の結果を Pull Request の差分に対するインラインコメントとして表示してくれるツールです。このツールにより Pull Request により生じる差分に対してのみ linter の出力結果を表示してやることができます。これにより「既存のコードベースに対する膨大な指摘点により CI が常に落ちた状態になり、誰も CI を気にしなくなる」というような事態を避けつつ、漸進的にコードのセキュリティを向上していくことができます。

一方 golangci-lint は Go コード向けの種々の linter を走らせるためのツールであり、gosec 以外の多種多様な linter を一括して起動するのに利用できます。今回の場合は以下の .golangci.yml により gosec を起動するように設定されています4

linters:
  disable-all: true
  enable:
    - gosec

issues:
  exclude-use-default: false

このような設定のもと、実際にいくつかのな不穏な変更を含む Pull Request を発行してみましょう。すると 以下の画像のようなレビューコメント が付与されるのが確認できます。確かに Pull Request により生じた Go コードの差分に対してセキュリティ面の指摘が行われていますね。

f:id:flattsecurity:20210223151641p:plain

gosec はカスタムルールの追加(要件2)こそ満たしにくいものの5、導入障壁が低いのがありがたいところです。また先述の例では Pull Request のインラインコメントを通して問題点を指摘する形を取りましたが、gosec は Static Analysis Results Interchange Format(SARIF)という静的解析器の標準的な出力フォーマットに対応しているため、GitHub Code Scanning 等の機能と連携させることもできます。

Ruleguard - Go による Go のパターンベースのチェック

簡単な項目のチェック(要件1)はさておき、Go コードに関する組織独自のルールを作成・適用したい(要件2)という場合には、go-ruleguard が利用できます。こちらは go/analysis ベースの Go コードの linter であり、lint 用ルールを Go コードで書けるのが特徴的です。

この go-ruleguard に関しても GitHub 上のリポジトリ例(security-aware-repo-examples/go-ruleguard) を用意しました。この例を用いて動作を確認してみましょう。

こちらのリポジトリ例においても golangci-lint を 以下の YAML のように設定しています。この設定により、golangci-lint が起動されると、go-critic を経由して Ruleguard が呼び出されるようになります。

linters:
  disable-all: true
  enable:
    - gocritic
linters-settings:
  gocritic:
    enabled-checks:
      - ruleguard
    settings:
      ruleguard:
        rules: ./rules/rules.go

上述の YAML ファイル中で Ruleguard にオプションとして渡されている ./rules/rules.go は以下のような Go コードです。

package gorules

import "github.com/quasilyte/go-ruleguard/dsl"

func rawSQL(m dsl.Matcher) {
    m.Import("gorm.io/gorm")
    m.Match(`$x := fmt.Sprintf($s, $*args); $*_; $db.Raw($x).$_($_)`).
        Where(m["db"].Type.Is("*gorm.DB")).
        Report(`A raw query is used: $s, $args`)
}

Ruleguard ではこのように、Go コードを通して Go コードに対する lint ルールを定義することが出来ます。例えば上述のルールは「以下の 2 つの条件を満たす Go コードが見つかった場合にアラートを報告する」というようなものになっています。

  1. Go 向け ORM である gorm.io/gorm を通して生の SQL クエリが発行されている
  2. 発行されているクエリが fmt.Sprintf() ような文字列合成操作に起因するものである

実際に この commit のような不穏な変更 を含む Pull Request を発行してみましょう。すると確かに 以下の画像のようなレビューコメント が付与されるのが確認できます。SQL Injection 脆弱性を含みうる実装が発見できました。

f:id:flattsecurity:20210223151939p:plain

このように go-ruleguard においては Go 言語を用いてGo 言語向けのルールを作成することになります。Go 言語に特化して設計されている分、細かいところに手が届きやすいもの特徴的です。

また、作成したルールはパッケージ化してやることもできます(参考)。これはリポジトリや組織を横断して作成したルールを積み上げていく上で有益です。

公式で Ruelguard by example というチュートリアルが提供されていますから、ぜひ一読して、その強力さを確認してみてください。

Semgrep - Go に限らない多種多様なコードのチェック

ここまで紹介した 2 つのツールのうち、gosec は手ぶらでの簡易的なチェック(要件1)に向いており、go-ruleguard はカスタムルールの作成(要件2)に向いているものでした。 これら 2 つを組み合わせることで、元々達成したかった要件 1, 2 は容易に満たすことが出来ます。

一方、選択肢は他にもあります。その一例が Semgrep です。Semgrep は様々なプログラミング言語のコードに対して柔軟な検索を行うことができる静的解析ツールであり、公式から公開されている便利なルールセット により手軽なセキュリティスキャンを行いながら、YAML ベースの DSL により自前のルールを定義できる(参考)のが特徴です。つまり、このツール 1 つで要件 1, 2 を満たすことができるのです。

ここまでと同様に、実際に Semgrep を設定した GitHub 上のリポジトリ例(security-aware-repo-examples/go-semgrep) を用いて、Semgrep の世界を覗いてみましょう。

以下は Semgrep でコードの問題を検出するためのルールの具体例です。このルールは先ほど同様に、gorm.io/gorm を通して怪しい生 SQL クエリを発行している箇所を検出することを目指したものです。

rules:
  - id: raw_sql
    languages:
      - go
    pattern: |
      $X = fmt.Sprintf($Q, ...)
      ...
      db.Raw($X)
    message: |
      A raw query is used: $Q.
    severity: WARNING

具体的には pattern プロパティがそのような箇所を検出するためのルールを定義しています。ここで$X$Q などは メタ変数(Metavariable) であり、定数式や関数呼び出しなどの自由なコード片をキャプチャするために利用されています。

メタ変数によりキャプチャされたコード片は他のプロパティの値の中で利用できます。上記のルール例では、pattern にマッチしたコード片が見つかった際のログメッセージを定義する message プロパティの中で、fmt.Sprintf() のテンプレート文字列として利用されている メタ変数 $Q を利用しています。

さて、このルールと適当な Go コードを 実際に Semgrep 公式で提供されている Playground に与えてみましょう。すると、以下の画像のような画面が表示されます。画像左下では確かに怪しい生 SQL クエリの発行をしているコード片がハイライトされていますし、画像右上のエラーメッセージ中には、確かに怪しいクエリ文字列が表示されていますね。

f:id:flattsecurity:20210223151713p:plain

先ほど示した例示用のリポジトリ では、Pull Request の発行時に上記のカスタムルールと gosec のルールを Semgrep で抜粋したルールセット による検査を行い6、その結果を reviewdog 経由でフィードバックできるように GitHub Actions の Workflow を定義してあります。

実際に これまた不穏な差分 を含む Pull Request を作成してみると、以下の画像に示すようなレビューコメントが得られます。確かに Pull Request の差分に対して、Semgrep 公式のルールによる指摘事項と、カスタムルールによる指摘事項がそれぞれ 1 つずつ表示されています。

f:id:flattsecurity:20210223152051p:plain

Semgrep は高度なコードに対するポリシー定義を可能にしてくれます。また Semgrep は公式のルールプリセットの拡充も進めているので、「まず公式のプリセットだけで運用を始めて、後から必要に応じてルールを追加していく」といった使い方がしやすいのも特徴的です。

それに、Semgrep は様々なプログラミング言語をサポートしてくれています7。これも嬉しい点の 1 つです。もちろん特定の言語にフォーカスしていない分、細かいところに手が届かないむず痒さを感じることはありますが、「複数のプログラミング言語を組織内・リポジトリ内で並行して利用する場合であってもコードへのポリシーを画一的に管理できる」というのは想像以上に便利なものです。

おわりに

本稿ではいくつかのツールを用いて、GitHub 上での Pull Request により生じた差分を対象にして、Go コードセキュリティ上の問題を検出・報告してくれる ようなガードレールを設置する例を示しました。セキュリティに関する静的解析ツールの利用は、どうしても既存コードに対する膨大な(False Positive を含む)指摘点の修正に手が回らないことが課題となりますが、コード差分に対してのみ警告を表示するアプローチからであれば小さく始められます。このような環境整備はセキュリティの専門家ではなくとも行えるものですから、ぜひ導入を検討してみてください。

補足: 開発時に必須のセキュリティ知識を得るためには

本稿で紹介したようなガードレールを適切に設定・運用していくためには、前提としてセキュリティに関する基礎知識が必要です。そのうちWeb アプリケーション開発の際に求められるセキュリティ知識は、弊社の提供する SaaS 型 eラーニングサービス「KENRO」にて学習していただくことができます。同サービスは デモアプリケーションへの攻撃演習脆弱なソースコードの修正演習(通称: 堅牢化演習) といったハンズオンを通して、Web アプリケーションにまつわる脆弱性の原理・原則セキュアコーディング を学んでいただける SaaS です。

※2021年4月にサービス名を「Flatt Security Learning Platform」から「KENRO(ケンロー)」に変更しています。

f:id:flattsecurity:20210223152254j:plain

ここで朗報です!なんと本日より、同サービスにおいて、Go 言語で書かれた脆弱な Web アプリケーションの修正演習(堅牢化演習) をご利用いただけるようになりました(参考: プレスリリース)。従来よりもさらに Go 言語フレンドリーになった KENRO にご興味をお持ちの方や、セキュリティを意識したプロダクト開発を実践したい方、既存のセキュリティ研修に課題を抱えているといった方は是非 サービス詳細ページ よりお問い合わせください。

またサービス紹介ページからお問合せの際に 「本ブログ記事を見た」とお知らせくださったご法人様には、同サービスの無償トライアル環境をご提供いたします。これは本稿をここまで読んでくださった方限定8の特典です。ぜひ KENRO の魅力を体感してください!


  1. ここでは開発プロセスに関しての論を展開していますが、運用の世界においても、「セキュリティのためのガードレールを設置する」という考え方は各所で見られるものです。歴史の長いものだと SELinux はまさにその一例と言えるでしょう。新しいものだと Gatekeeperを始めとした Kubernetes 関連ツールや、同ツールも利用している Open Policy Agent(OPA)を中心としたエコシステムも、近年非常に大きな盛り上がりを見せています。

  2. ここで紹介する方法は当然他の CI プラットフォーム上でも利用できます。

  3. Go 言語の標準パッケージには Go コードの静的解析に利用できる様々なユーティリティが含まれており、Go 製の Go コード向け linter の多くはこのようなパッケージを用いて作成されています。 go/typesgo/astgo/analysis などが静的解析用のパッケージの例です。もちろん、これらのパッケージは自分で linter やセキュリティツールを作成するのにも利用できますから、興味がある方はぜひ詳細を調べてみて下さい。

  4. 説明の都合上 golangci-lint が持つほとんどの linter を off にしています。

  5. gosec の個別の検出事項は rules 以下に定義されています。本稿執筆時点では gosec のコードベースは極めてシンプルなので、カスタムルールの追加も不可能ではありません。

  6. 例えば Go コードに直ちに利用できるルールは この検索ページreturntocorp/semgrep-rules の go/ 以下 から確認できます。

  7. 開発元の r2c の GitHub Organization を見ると分かる通り、彼らはとても地道にパーサを実装しています。すごい。

  8. 厳密には KENRO の Go 言語対応に関する プレスリリース においても同様の特典を提供しています。