govulncheckを利用したGitHub Actionsでの脆弱性チェック

こんにちは、プロダクト開発部コアグループの井上です。
コアグループでは、次世代ECの開発を行っています。

今回はgovulncheckとGitHub ActionsをつかってGoの脆弱性チェックを自動化した取り組みについて紹介します。

govulncheck

govulncheckは、Goプロジェクトの依存関係とソースコードを分析し、既知の脆弱性を検出するツールです。コマンドラインから簡単に実行することができます。

今回はCVE-2024-24789を含むこちらのサンプルを想定して書いていきます。

> go version
go version go1.22.3 windows/amd64
vulnsample/
├── cmd/
│   └── main.go
├── util.go
└── go.mod
module vulnsample

go 1.22
package main

import (
    "fmt"
    "os"
    "log/slog"

    "vulnsample"
)

func main() {
    slog.Info("Start")

    f, err := os.Open("path/to/zipfile.zip")
    if err != nil {
        panic(err)
    }
    defer f.Close()

    fi, err := f.Stat()
    if err != nil {
        panic(err)
    }

    result, err := vulnsample.Unzip(f, fi.Size())
    if err != nil {
        panic(err)
    }

    fmt.Println(result)
}
package vulnsample

import (
    "archive/zip"
    "io"
)

func Unzip(zipStream io.ReaderAt, size int64) (map[string][]byte, error) {
    zipReader, err := zip.NewReader(zipStream, size)
    if err != nil {
        return nil, err
    }

    contents := make(map[string][]byte)
    for _, zipFile := range zipReader.File {
        f, err := zipFile.Open()
        if err != nil {
            return nil, err
        }
        defer f.Close()

        data, err := io.ReadAll(f)
        if err != nil {
            return nil, err
        }

        contents[zipFile.Name] = data
    }

    return contents, nil
}

実行方法

go install golang.org/x/vuln/cmd/govulncheck@latest
> govulncheck ./...
=== Symbol Results ===

Vulnerability #1: GO-2024-2888
    Mishandling of corrupt central directory record in archive/zip
  More info: https://pkg.go.dev/vuln/GO-2024-2888
  Standard library
    Found in: archive/zip@go1.22.3
    Fixed in: archive/zip@go1.22.4
    Example traces found:
      #1: util.go:10:33: vulnsample.Unzip calls zip.NewReader

Your code is affected by 1 vulnerability from the Go standard library.
This scan also found 0 vulnerabilities in packages you import and 2
vulnerabilities in modules you require, but your code doesn't appear to call
these vulnerabilities.
Use '-show verbose' for more details.

-jsonオプションを利用することで、結果をjson形式で得ることができます。

{
  "config": {
    "protocol_version": "v1.0.0",
    "scanner_name": "govulncheck",
    "scanner_version": "v1.1.2",
    "db": "https://vuln.go.dev",
    "db_last_modified": "2024-07-02T20:11:00Z",
    "go_version": "go1.22.3",
    "scan_level": "symbol",
    "scan_mode": "source"
  }
}
{
  "progress": {
    "message": "Scanning your code and 58 packages across 1 dependent module for known vulnerabilities..."
  }
}
{
  "progress": {
    "message": "Fetching vulnerabilities from the database..."
  }
}
{
  "osv": {
    "schema_version": "1.3.1",
    "id": "GO-2021-0067",
    "modified": "2024-05-20T16:03:47Z",
    "published": "2021-04-14T20:04:52Z",
    "aliases": [
      "CVE-2021-27919"
    ],
    "summary": "Panic when opening archives in archive/zip",
    "details": "Using Reader.Open on an archive containing a file with a path prefixed by \"../\" will cause a panic due to a stack overflow. If parsing user supplied archives, this may be used as a denial of service vector.",
    "affected": [
      {
        "package": {
          "name": "stdlib",
          "ecosystem": "Go"
        },
        "ranges": [
          {
            "type": "SEMVER",
            "events": [
              {
                "introduced": "1.16.0-0"
              },
              {
                "fixed": "1.16.1"
              }
            ]
          }
        ],
        "ecosystem_specific": {
          "imports": [
            {
              "path": "archive/zip",
              "symbols": [
                "toValidName"
              ]
            }
          ]
        }
      }
    ],
    "references": [
      {
        "type": "FIX",
        "url": "https://go.dev/cl/300489"
      },
      {
        "type": "FIX",
        "url": "https://go.googlesource.com/go/+/cd3b4ca9f20fd14187ed4cdfdee1a02ea87e5cd8"
      },
      {
        "type": "REPORT",
        "url": "https://go.dev/issue/44916"
      },
      {
        "type": "WEB",
        "url": "https://groups.google.com/g/golang-announce/c/MfiLYjG-RAw/m/zzhWj5jPAQAJ"
      }
    ],
    "database_specific": {
      "url": "https://pkg.go.dev/vuln/GO-2021-0067",
      "review_status": "REVIEWED"
    }
  }
}
{
  "osv": {
    "schema_version": "1.3.1",
    "id": "GO-2024-2888",
    "modified": "2024-06-04T22:48:55Z",
    "published": "2024-06-04T22:48:55Z",
    "aliases": [
      "CVE-2024-24789"
    ],
    "summary": "Mishandling of corrupt central directory record in archive/zip",
    "details": "The archive/zip package's handling of certain types of invalid zip files differs from the behavior of most zip implementations. This misalignment could be exploited to create an zip file with contents that vary depending on the implementation reading the file. The archive/zip package now rejects files containing these errors.",
    "affected": [
      {
        "package": {
          "name": "stdlib",
          "ecosystem": "Go"
        },
        "ranges": [
          {
            "type": "SEMVER",
            "events": [
              {
                "introduced": "0"
              },
              {
                "fixed": "1.21.11"
              },
              {
                "introduced": "1.22.0-0"
              },
              {
                "fixed": "1.22.4"
              }
            ]
          }
        ],
        "ecosystem_specific": {
          "imports": [
            {
              "path": "archive/zip",
              "symbols": [
                "NewReader",
                "OpenReader",
                "findSignatureInBlock"
              ]
            }
          ]
        }
      }
    ],
    "references": [
      {
        "type": "FIX",
        "url": "https://go.dev/cl/585397"
      },
      {
        "type": "REPORT",
        "url": "https://go.dev/issue/66869"
      },
      {
        "type": "WEB",
        "url": "https://groups.google.com/g/golang-announce/c/XbxouI9gY7k/m/TuoGEhxIEwAJ"
      }
    ],
    "credits": [
      {
        "name": "Yufan You (@ouuan)"
      }
    ],
    "database_specific": {
      "url": "https://pkg.go.dev/vuln/GO-2024-2888",
      "review_status": "REVIEWED"
    }
  }
}
{
  "progress": {
    "message": "Checking the code against the vulnerabilities..."
  }
}
{
  "finding": {
    "osv": "GO-2024-2887",
    "fixed_version": "v1.22.4",
    "trace": [
      {
        "module": "stdlib",
        "version": "v1.22.3"
      }
    ]
  }
}
{
  "finding": {
    "osv": "GO-2024-2888",
    "fixed_version": "v1.22.4",
    "trace": [
      {
        "module": "stdlib",
        "version": "v1.22.3"
      }
    ]
  }
}
{
  "finding": {
    "osv": "GO-2024-2963",
    "fixed_version": "v1.22.5",
    "trace": [
      {
        "module": "stdlib",
        "version": "v1.22.3"
      }
    ]
  }
}
{
  "finding": {
    "osv": "GO-2024-2888",
    "fixed_version": "v1.22.4",
    "trace": [
      {
        "module": "stdlib",
        "version": "v1.22.3",
        "package": "archive/zip"
      }
    ]
  }
}
{
  "finding": {
    "osv": "GO-2024-2888",
    "fixed_version": "v1.22.4",
    "trace": [
      {
        "module": "stdlib",
        "version": "v1.22.3",
        "package": "archive/zip",
        "function": "NewReader",
        "position": {
          "filename": "src/archive/zip/reader.go",
          "offset": 3044,
          "line": 106,
          "column": 6
        }
      },
      {
        "module": "vulnsample",
        "package": "vulnsample",
        "function": "Unzip",
        "position": {
          "filename": "util.go",
          "offset": 189,
          "line": 10,
          "column": 33
        }
      }
    ]
  }
}
osv

OSV (Open Software Vulnerabilities) は、Googleが管理しているオープンソースソフトウェアの脆弱性を追跡するためのデータベースで、 出力結果として得られるosvそのフォーマット(OSV Schema)に沿ったGoの脆弱性情報です。 govulncheckの実装 を見ると、スキャン対象のモジュールに影響がありそうなものがosvとして出力されているようでした。
※記事に乗せるには量が多すぎるため、上記のサンプルでは削っています。

affected.rangesを見ることで修正バージョンが分かります。

finding

スキャンの結果見つかった脆弱性findingとして出力されます。 symbolレベルでスキャンを実行した場合(デフォルト)、trace内に影響を受けるコードの情報が入ってきます。
今回はutil.goの10行目でzip.NewReaderを使用しているためGO-2024-2888の影響があるということが分かります。

今回作ったもの

GitHub Actionsであればアクションが公開されているため、簡単にCIに組み込むことができますが、 今回はISSUEの作成まで自動で行いたかったので、govulncheck ./... -jsonの結果を自前でパースすることにしました。

jsonのパース

govulncheck ./... -jsonの結果はjson.Unmarshalではパースすることはできません。
当初はbufio.Scanner.Scan()を使って力業でパースしましたが、json.Decoderを使用することで簡単にパースすることができます。

package main

import (
    "encoding/json"
    "fmt"
    "os"
    "path/filepath"
)

func main() {
    home, _ := os.UserHomeDir()

    vulncheckResult, err := os.Open(filepath.Join(home, "workspace", "govulncheck.json"))
    if err != nil {
        panic(err)
    }

    result := make([]map[string]any, 0)
    dec := json.NewDecoder(vulncheckResult)
    for dec.More() {
        m := make(map[string]any)
        if err := dec.Decode(&m); err != nil {
            panic(err)
        }

        if len(m) > 0 {
            fmt.Println(m)
            result = append(result, m)
        }
    }

    _ = result
}
map[config:map[db:https://vuln.go.dev db_last_modified:2024-07-02T20:11:00Z go_version:go1.22.3 protocol_version:v1.0.0 scan_level:symbol scan_mode:source scanner_name:govulncheck scanner_version:v1.1.2]]
map[progress:map[message:Scanning your code and 58 packages across 1 dependent module for known vulnerabilities...]]
map[progress:map[message:Fetching vulnerabilities from the database...]]
map[osv:map[affected:[map[ecosystem_specific:map[imports:[map[path:archive/zip symbols:[toValidName]]]] package:map[ecosystem:Go name:stdlib] ranges:[map[events:[map[introduced:1.16.0-0] map[fixed:1.16.1]] type:SEMVER]]]] aliases:[CVE-2021-27919] database_specific:map[review_status:REVIEWED url:https://pkg.go.dev/vuln/GO-2021-0067] details:Using Reader.Open on an archive containing a file with a path prefixed by "../" will cause a panic due to a stack overflow. If parsing user supplied archives, this may be used as a denial of service vector. id:GO-2021-0067 modified:2024-05-20T16:03:47Z published:2021-04-14T20:04:52Z references:[map[type:FIX url:https://go.dev/cl/300489] map[type:FIX url:https://go.googlesource.com/go/+/cd3b4ca9f20fd14187ed4cdfdee1a02ea87e5cd8] map[type:REPORT url:https://go.dev/issue/44916] map[type:WEB url:https://groups.google.com/g/golang-announce/c/MfiLYjG-RAw/m/zzhWj5jPAQAJ]] schema_version:1.3.1 summary:Panic when opening archives in archive/zip]]
map[osv:map[affected:[map[ecosystem_specific:map[imports:[map[path:archive/zip symbols:[NewReader OpenReader findSignatureInBlock]]]] package:map[ecosystem:Go name:stdlib] ranges:[map[events:[map[introduced:0] map[fixed:1.21.11] map[introduced:1.22.0-0] map[fixed:1.22.4]] type:SEMVER]]]] aliases:[CVE-2024-24789] credits:[map[name:Yufan You (@ouuan)]] database_specific:map[review_status:REVIEWED url:https://pkg.go.dev/vuln/GO-2024-2888] details:The archive/zip package's handling of certain types of invalid zip files differs from the behavior of most zip implementations. This misalignment could be exploited to create an zip file with contents that vary depending on the implementation reading the file. The archive/zip package now rejects files containing these errors. id:GO-2024-2888 modified:2024-06-04T22:48:55Z published:2024-06-04T22:48:55Z references:[map[type:FIX url:https://go.dev/cl/585397] map[type:REPORT url:https://go.dev/issue/66869] map[type:WEB url:https://groups.google.com/g/golang-announce/c/XbxouI9gY7k/m/TuoGEhxIEwAJ]] schema_version:1.3.1 summary:Mishandling of corrupt central directory record in archive/zip]]
map[progress:map[message:Checking the code against the vulnerabilities...]]
map[finding:map[fixed_version:v1.22.4 osv:GO-2024-2887 trace:[map[module:stdlib version:v1.22.3]]]]
map[finding:map[fixed_version:v1.22.4 osv:GO-2024-2888 trace:[map[module:stdlib version:v1.22.3]]]]
map[finding:map[fixed_version:v1.22.5 osv:GO-2024-2963 trace:[map[module:stdlib version:v1.22.3]]]]
map[finding:map[fixed_version:v1.22.4 osv:GO-2024-2888 trace:[map[module:stdlib package:archive/zip version:v1.22.3]]]]
map[finding:map[fixed_version:v1.22.4 osv:GO-2024-2888 trace:[map[function:NewReader module:stdlib package:archive/zip position:map[column:6 filename:src/archive/zip/reader.go line:106 offset:3044] version:v1.22.3] map[function:Unzip module:vulnsample package:vulnsample position:map[column:33 filename:util.go line:10 offset:189]]]]]

Process finished with the exit code 0

ISSUEの作成

パース結果をもとに、リポジトリのコードに影響があるtrace.positionが存在する脆弱性を集計し、ISSUE用のmarkdownに成形して、actions/github-scriptでISSUEを作成しています。

GitHub Actionsで作成された脆弱性対応ISSUE

余談ですがaliasに入ってくるCVEは、CVE-2024-24786などをそのままISSUE張り付けただけでリンクになって便利でした。

おわりに

今回は脆弱性があっても、対象の関数などを使用していないなどで影響がない場合はISSUEを作成しないようにしましたが、結局別チームが別途拾って対応することになってしまいました。
修正版リリースから最速で対応できるよう、findingがある場合は無条件にISSUEが作成されるように変更予定です。

また、現在は一度出た脆弱性を保存する仕組みを用意していない為、重複したISSUEが作られてしまわないようにスキャン頻度を週一回にしています。 毎日実行しても問題ないようこの点も改善していく予定です。

皆さんも手軽に脆弱性チェックができるgovulncheckを活用してみてください。