govulncheck はなぜ賢いのか¶
コールグラフで変わる脆弱性検出の世界
Go Conference 2026 — yoshioka0101
自己紹介¶
yoshioka0101
- バックエンドエンジニア
- Go で REST API を書いている
- Go のソースコードを読むのが好き
※ Go コアチームではありません。
一次資料(ソース・issue・blog)を読んで理解したことを話します。
一次資料(ソース・issue・blog)を読んで理解したことを話します。
今日話すこと¶
1. naive な脆弱性チェックの問題
go.sum を見るだけでは何が足りないか
2. govulncheck のアプローチ
reachability とは何か
3. 内部実装を読む
SSA・コールグラフ・interface 問題
あなたの go.sum に CVE がある¶
$ go list -m -json all | govulncheck -json -
# もしくは単純に
$ grep "golang.org/x/crypto" go.sum
golang.org/x/crypto v0.12.0 h1:...
CVE-2023-XXXX: golang.org/x/crypto < v0.17.0
脆弱な関数が含まれています
→ 本当に危険ですか?
naive な脆弱性チェックの問題¶
❌ パッケージベースのチェック
「このパッケージを使っている」
→「脆弱性がある」
問題:
脆弱な関数を実際に呼んでいるかは関係ない
→ 大量の false positive
✅ govulncheck のアプローチ
「脆弱な関数が
あなたのコードから呼ばれているか」
を判定する
= reachability analysis
reachability とは¶
あなたのコード
└─ http.HandleFunc
└─ handler()
└─ bcrypt.GenerateFromPassword() ← crypto を使っている
└─ (内部実装)
脆弱な関数: ssh.NewClientConn() ← 呼ばれていない
govulncheck の判定
ssh.NewClientConn() はあなたのエントリポイントから到達不可能
→ この CVE はあなたには影響しない
→ この CVE はあなたには影響しない
では、どうやって判定するのか¶
🔍
コールグラフ (Call Graph)
「どの関数がどの関数を呼ぶか」を
プログラム全体で静的に解析した有向グラフ
プログラム全体で静的に解析した有向グラフ
govulncheck の内部実装:全体像¶
golang.org/x/vuln
├── cmd/govulncheck/ エントリポイント
├── internal/vulncheck/ コア分析ロジック
│ ├── source.go ソース解析モード
│ └── binary.go バイナリ解析モード
└── internal/callgraph/ コールグラフ構築
コアは
ここで「SSA に変換 → コールグラフ構築 → 脆弱関数への到達判定」が行われる
internal/vulncheck/source.goここで「SSA に変換 → コールグラフ構築 → 脆弱関数への到達判定」が行われる
SSA(Static Single Assignment)¶
変数への代入が一度だけ。
どの値がどこから来たかが自明になる → 静的解析がしやすい
どの値がどこから来たかが自明になる → 静的解析がしやすい
SSA → コールグラフへ¶
// SSA に変換した後、各 Call 命令を収集する
for _, block := range fn.Blocks {
for _, instr := range block.Instrs {
if call, ok := instr.(ssa.CallInstruction); ok {
// 呼び出し先を記録 → グラフのエッジを追加
}
}
}
internal/callgraph/ で CHA (Class Hierarchy Analysis) またはVTA (Variable Type Analysis) を使ってグラフを構築する
interface が難しい¶
type Store interface {
Get(key string) ([]byte, error)
}
func process(s Store) {
s.Get("key") // どの実装を呼ぶ?
}
問題
interface 越しの呼び出しは、静的解析だけでは呼び先が特定できない→ 全実装を候補にすると false positive が増える
→ 実装を絞りすぎると false negative が増える
VTA(Variable Type Analysis)で解く¶
process() が呼ばれる文脈を追う
main() → process(&RedisStore{})
→ process(&MemStore{})
↓ VTA はこれを収集する
s.Get() の候補 = { RedisStore.Get, MemStore.Get }
↑ MockStore.Get は候補から外れる
プログラム全体のデータフローを見て、実際に渡される型だけに絞る
→ false positive を減らせる
→ false positive を減らせる
source モード vs binary モード¶
source モード
- ソースコードがある
- SSA + VTA で精密なコールグラフ
- 行レベルで脆弱な呼び出しを特定
- 精度: 高い
binary モード
- コンパイル済みバイナリのみ
- シンボルテーブルから関数を抽出
- コールグラフは作れない
- 精度: 低い(false positive あり)
自分のコードで試す¶
$ govulncheck ./...
Vulnerability #1: GO-2023-1840
Timing sidechannel attack in golang.org/x/crypto/ssh
More info: https://pkg.go.dev/vuln/GO-2023-1840
Module: golang.org/x/crypto
Found in: golang.org/x/crypto/ssh@v0.12.0
Fixed in: golang.org/x/crypto@v0.17.0
Call stacks in your code:
main.go:34:18: myapp.NewServer calls ssh.NewClientConn
↑ これがあると危険
Call stacks が出る = 実際に到達可能
Call stacks が出ない = パッケージはあるが影響なし
Call stacks が出ない = パッケージはあるが影響なし
まとめ:govulncheck はなぜ賢いのか¶
1️⃣
ソースを SSA に変換する
データフローが追いやすい中間表現へ
2️⃣
VTA でコールグラフを構築する
interface 越しの呼び出しを型情報で絞り込む
3️⃣
脆弱な関数への到達可能性を判定する
呼ばれていない脆弱性は報告しない
Go Far, Go Together¶
ツールを使うだけでなく
「なぜこう動くのか」を知ること
「なぜこう動くのか」を知ること
それが、チームで安全に遠くまで行く力になる
golang.org/x/vuln — ソースを読んでみてください
参考資料¶
公式
- go.dev/blog/vuln — govulncheck 設計ブログ
- go.dev/blog/govulncheck — 詳細解説
- github.com/golang/vuln — ソースコード
- pkg.go.dev/golang.org/x/vuln — ドキュメント
内部実装を読む入口
- golang.org/x/tools/go/ssa — SSA 変換
- golang.org/x/tools/go/callgraph/vta — VTA アルゴリズム
- golang.org/x/tools/go/callgraph/cha — CHA アルゴリズム