every Tech Blog

株式会社エブリーのTech Blogです。

govulncheckで行う脆弱性対応

govulncheckで行う脆弱性対応

はじめに

開発本部でデリッシュキッチンプレミアム会員向けの開発を担当しているhondです!
先日axiosのサプライチェーン攻撃が話題になりました。axiosのリードメンテナのnpmアカウントがソーシャルエンジニアリング経由で侵害され、悪意のあるバージョン(1.14.10.30.4)が約3時間npmに公開されていたというもので、詳細はaxios公式のPost Mortemにまとまっています。広く使われているHTTPクライアントが直接狙われた事件で、エコシステムに依存する側としても他人事ではないなと感じました。
これを受けて、普段業務で利用しているGoではどのような脆弱性対策が取られているのか、また開発者としてどのような運用が推奨されているのかを改めて確認しました。結論として、Goではサプライチェーン攻撃自体はgo.sumとChecksum Database(sum.golang.org)によってエコシステム側で既に対策されています。本記事ではその前提の上で、開発者側が実運用で何を行えるのか、govulncheckDependabotの組み合わせによるCI運用方法をまとめます。

Goの脆弱性対応 Best Practices

Goのセキュリティ対策全般については、公式のSecurityページにまとめられています。このページでは脆弱性管理・Fuzzing・暗号化ライブラリ・Go自体のセキュリティポリシーなどについて確認することができ、そのひとつにSecurity Best Practices for Go Developersがあります。
Best Practicesでは以下の6項目が推奨されています。

  1. ソースコードおよびバイナリの脆弱性スキャン(govulncheck
  2. Go本体および依存関係のアップデート
  3. Fuzzingによるエッジケース脆弱性の発見
  4. Race detectorによる競合状態の検出
  5. go vetによる疑わしい構成の検査
  6. golang-announceメーリングリストの購読

本記事ではこのうち脆弱性スキャンに焦点を当てて、CIでの運用とDependabotとの使い分けを整理します。

govulncheck とは

govulncheckはGoの脆弱性スキャナとして公式が提供するCLIツールです。一般的な依存関係スキャナとの大きな違いは、バージョン比較ではなく、脆弱性のある関数が実際に呼び出されているかを解析する点にあります。

analyzes your codebase and only surfaces vulnerabilities that actually affect you, based on which functions in your code are transitively calling vulnerable functions (Go Vulnerability Managementより)

つまり、脆弱性のある関数が依存パッケージに含まれているだけでなく、自分のコードからその関数が実際に(推移的にも)呼ばれている場合にのみ検知します。これによってパッケージを取り込んでいるが該当機能は使っていないというケースでは警告が出ず、本当に対応すべき脆弱性を優先度高く扱えます。

Go Vulnerability Database

govulncheckGo Vulnerability Database(vuln.go.devをAPI経由で参照して脆弱性情報を取得しています。このデータベースはGo Security Teamによって運営されており、以下のデータソースから集めた情報が登録されています。

  • NVD(National Vulnerability Database)
  • GitHub Advisory Database
  • Goパッケージメンテナからの直接報告

取り込まれた情報はOSV formatに整形され、API経由で公開されます。

Go Vulnerability Database Architecture

Severityラベル

Go Vulnerability Databaseは「LOW」「CRITICAL」といったSeverityラベルを提供していません。公式では以下のように説明されています。

We believe good descriptions of vulnerabilities are more useful than severity indicators. (Go Vulnerability Managementより)

脆弱性の影響はパッケージがどのように使われているかで大きく変わります。例えばパーサーのクラッシュを引き起こす脆弱性は、外部入力を処理する箇所では深刻ですが、ローカル設定ファイルの読み込みにのみ使っている場合は影響が軽微です。普遍的なSeverity指標は誤解を招く可能性があるため、Goでは脆弱性そのものの詳細な説明に加えて、govulncheckが実際の呼び出し経路(stack trace)と該当箇所を出力することで、利用者自身が影響を判断できるという設計が採られています。

実践

以下は脆弱性に到達可能かによってgovulncheckの出力がどう変わるかを確認したものになります。どちらもgolang.org/x/text@v0.3.5GO-2021-0113の対象バージョン)に依存していますが、reachable側は脆弱性のあるlanguage.Parseを呼び、unreachable側はimportするだけで呼び出しません。

reachable

// go.mod
module reachable-sample

go 1.26

require golang.org/x/text v0.3.5
// main.go
func main() {
    tag, err := language.Parse("en-US")
    if err != nil {
        fmt.Println("parse error:", err)
        return
    }
    fmt.Println("parsed:", tag)
}

コマンド実行

$ govulncheck ./...

出力

=== Symbol Results ===

Vulnerability #1: GO-2021-0113
    Out-of-bounds read in golang.org/x/text/language
  More info: https://pkg.go.dev/vuln/GO-2021-0113
  Module: golang.org/x/text
    Found in: golang.org/x/text@v0.3.5
    Fixed in: golang.org/x/text@v0.3.7
    Example traces found:
      #1: main.go:10:28: reachable.main calls language.Parse

Your code is affected by 1 vulnerability from 1 module.
This scan also found 3 vulnerabilities in packages you import and 10
vulnerabilities in modules you require, but your code doesn't appear to call
these vulnerabilities.

main.go:10:28: reachable.main calls language.Parseという呼び出し経路と、Found in: v0.3.5 / Fixed in: v0.3.7という修正先バージョンが具体的に示されます。Your code is affected by 1 vulnerabilityと明示されるので対応すべき箇所もすぐに分かります。

unreachable

// main.go
func main() {
    // golang.org/x/text をimportしているが、脆弱性のある language.Parse は呼ばない
    tag := language.English
    fmt.Println("tag:", tag)
}

出力

=== Symbol Results ===

No vulnerabilities found.

Your code is affected by 0 vulnerabilities.
This scan also found 4 vulnerabilities in packages you import and 10
vulnerabilities in modules you require, but your code doesn't appear to call
these vulnerabilities.

同じ脆弱なバージョンに依存していても、language.Parseを呼び出していないのでNo vulnerabilities foundとなります。一方でThis scan also found 4 vulnerabilities in packages you import and 10 vulnerabilities in modules you requireとあり、「importはしている/moduleには含まれている」レベルの脆弱性がそれぞれ何件あるかも合わせて確認できます。関数が実際に呼び出されているかまで見ることで、本当に対応すべき脆弱性の優先度がつけやすくなっています。

実行環境

govulncheckは下記の方法で利用することが可能です。

  • CLI : go install golang.org/x/vuln/cmd/govulncheck@latestでインストールしてgovulncheck ./...を実行する
  • VS Code拡張 : Go公式拡張(golang.go)の設定で"go.diagnostic.vulncheck": "Imports"を有効にすると、編集中のファイルに対して診断が表示される
  • pkg.go.dev : 各パッケージページのVulnerabilitiesタブからそのモジュールに紐づくアドバイザリを確認できる
  • CI(GitHub Actions): golang-govulncheck-actionを使ってPull Requestごとにスキャンを回せる

ローカルや拡張機能でも早い段階で脆弱性を検知できますが、継続的にブランチ全体をカバーするにはやはりCIに組み込むのが確実です。以降はCIでの利用方法について説明します。

CIでの利用

golang-govulncheck-action の利用

Goチームが公式に提供しているgolang/govulncheck-actionを導入することでCI上でgovulncheckが利用できます。
READMEにも記載されている通り、このアクションは現時点ではexperimentalステータスで提供されています。
最小構成は以下のようになります。

# .github/workflows/govulncheck.yml
name: govulncheck

on:
  pull_request:
  push:
    branches: [main]

jobs:
  govulncheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: golang/govulncheck-action@v1
        with:
          go-version-input: '1.26'

これだけでPull Requestごとにgovulncheckが走り、到達可能な脆弱性が見つかるとジョブが失敗します。

出力フォーマット

CLI版のgovulncheckには-formatオプションがあり、出力形式をtext,json,sarifから選べます。golang/govulncheck-actionでも同様にoutput-formatオプションでフォーマットを切り替えることができます。このフォーマットの選択に応じてExit Codeも変わるので、CIのジョブ成否にもそのまま影響します。

output-format Exit Code ジョブの挙動 出力
text(デフォルト) 脆弱性検知時に3 脆弱性が見つかるとジョブが失敗する Actionsのログに直接出力
json 常に0 脆弱性が見つかってもジョブは成功する Actionsのログに直接出力(JSON)
sarif 常に0 脆弱性が見つかってもジョブは成功する output-fileで指定したファイルに出力

jsonsarifで常にExit Code 0になるのは、SARIFアップロード、PRコメントなどにパイプで渡すことを想定しているためです。脆弱性が検知されたらCIを落としたいのか、検知結果を別の場所に集約したいのかで使い分けることになります。

GitHub Securityタブの利用

検知結果をGitHubのSecurityタブ(Code scanning alerts)に集約したい場合は、sarif出力をgithub/codeql-action/upload-sarifでアップロードする構成が使えます。この構成はリポジトリでCode scanning alertsが有効になっていることが前提になります(Settings > Code security and analysis)。有効になっていない場合、SARIFのアップロード自体はできてもSecurityタブにalertとして表示されません。

    permissions:
      contents: read
      security-events: write
      actions: read
    steps:
      - uses: golang/govulncheck-action@v1
        with:
          go-version-input: '1.26'
          output-format: sarif
          output-file: govulncheck.sarif
      - uses: github/codeql-action/upload-sarif@v4
        with:
          sarif_file: govulncheck.sarif

permissionsを明示しているのは、upload-sarifがSecurityタブへの書き込みにsecurity-events: writeを必要とするためです。actions: readもプライベートリポジトリでのSARIF取り込みに必要になります。このようにすると、検知された到達可能な脆弱性がSecurityタブにalertとして積まれ、過去の検知履歴やステータスもそこから追跡できます。

govulncheck と Dependabot の使い分け

ここまでgovulncheckをCIで実行する話をしてきましたが、最後にDependabotとの違いについて簡単にまとめます。

  • Go Vulnerability Database(vuln.go.dev)に登録された脆弱性は、GitHub Advisory Databaseにも取り込まれる
  • DependabotはGitHub Advisory Databaseを参照して、依存関係が脆弱性のあるバージョンに該当する場合にPRやアラートを生成する

つまりgovulncheckDependabotは、参照している脆弱性情報は実質同じで、検知のアプローチと役割が違うツールになっています。
Dependabotはバージョン比較なので、golang.org/x/text@v0.3.5に依存していれば関数を呼んでいなくてもアラートが飛びます。一方govulncheckは関数が実際に呼び出されているかまで見て、本当に対応が必要なケースだけをaffectedとして出力します。この性質の違いから、Dependabotには日常的な依存アップデートの自動化を任せ、govulncheckで実コードに影響する脆弱性を絞り込む、という役割分担で組み合わせるのが良いのかなと考えています。

まとめ

コードを書く中で他のpackageに依存しない実装はほぼ不可能なのでversion管理を行っていくのは大切だと思いますが、優先度を上げにくい部分だと感じています。その中でもgovulncheckを用いることで実際に到達可能でプロダクトに影響を与えるか明確にできるのは調査や対応優先度を他の人に伝える際のコストを下げることができるのでとても有用だと感じました。VSCodeの拡張などで開発時点で検証可能ですが、エージェントでのコーディングが増えIDEを開く機会も減ったのでpre-commitでCLIを実行する必要もあるのかなと思っています。チーム内でもDependabotの導入でPRは作成できているが詳細は追えておらず放置されるみたいな状態も度々あるので、GitHub Securityタブへ出力される設定を展開していく予定です。
ここまで読んでいただきありがとうございます!Goで脆弱性対策を行おうとしている人の助けになったら幸いです。