Mọi dự án đều có phụ thuộc, và phụ thuộc có lỗ hổng bảo mật (CVE). Scanner bảo mật truyền thống hoạt động đơn giản: nếu bạn dùng một phiên bản thư viện nằm trong danh sách CVE, nó báo động — bất kể code bạn có thực sự dùng hàm bị lỗi hay không. Kết quả là một biển cảnh báo, phần lớn là dương tính giả (bạn import thư viện nhưng không chạm tới phần lỗi), khiến đội bảo mật mệt mỏi và bỏ sót cảnh báo thật.
govulncheck (công cụ chính thức của Go) thông minh hơn hẳn: nó phân tích call graph — dựng đồ thị lời gọi từ code của bạn — và chỉ báo lỗ hổng mà code bạn thực sự gọi tới, kèm đường dẫn gọi chính xác. Bài này chạy govulncheck thật trên code dùng thư viện có lỗ hổng đã biết, và cho thấy nó tách bạch "lỗ hổng bạn gọi tới" (phải sửa) khỏi "lỗ hổng chỉ có mặt nhưng không chạm" (ưu tiên thấp) như thế nào.
Vì sao govulncheck khác scanner thường
// Scanner thường: có phụ thuộc X phiên bản dính CVE -> báo động,
// dù code bạn KHÔNG dùng hàm bị lỗi -> nhiễu cảnh báo giả.
// govulncheck: phân tích CALL GRAPH -> chỉ báo lỗ hổng mà code
// bạn THỰC SỰ gọi tới, kèm đường dẫn gọi chính xác. Ít nhiễu.
Code dùng phụ thuộc cũ có lỗ hổng
import "gopkg.in/yaml.v2" // v2.2.1 - phiên bản cũ dính CVE
func main() {
var m map[string]interface{}
// GỌI Unmarshal - hàm dính lỗ hổng tiêu thụ tài nguyên
_ = yaml.Unmarshal([]byte("a: 1\nb: 2\n"), &m)
}
Ta cố tình dùng yaml.v2 v2.2.1 (một phiên bản cũ) và gọi yaml.Unmarshal — hàm nằm trong đường dẫn của nhiều lỗ hổng đã biết. Chạy govulncheck ./... sẽ tải cơ sở dữ liệu lỗ hổng từ vuln.go.dev, dựng call graph, và đối chiếu.

Hình 1: govulncheck phân tích call graph — thay vì báo mọi phụ thuộc dính CVE, nó chỉ báo lỗ hổng mà code thực sự gọi tới (kèm trace), tách khỏi lỗ hổng chỉ-có-mặt-không-chạm để giảm nhiễu cảnh báo giả.
Đo thật: lỗ hổng gọi-tới vs chỉ-có
Chạy govulncheck ./..., nó báo các lỗ hổng code thực sự gọi tới, mỗi cái kèm trace chính xác:
Vulnerability #1: GO-2022-0956
Excessive resource consumption in gopkg.in/yaml.v2
Found in: yaml.v2@v2.2.1
Fixed in: yaml.v2@v2.2.4
Trace: main.go:11:20: vuln2.main calls yaml.Unmarshal
Vulnerability #2: GO-2021-0061 Denial of service
Found in v2.2.1 -> Fixed in v2.2.3 (main.go:11 calls Unmarshal)
Vulnerability #3: GO-2020-0036 Excessive resource consumption
Found in v2.2.1 -> Fixed in v2.2.8 (main.go:11 calls Unmarshal)
Ba thông tin cực kỳ hữu ích cho mỗi lỗ hổng: mã CVE (GO-2022-0956...) để tra chi tiết, phiên bản dính và phiên bản vá (Found in v2.2.1 -> Fixed in v2.2.4) để biết cần nâng lên đâu, và trace chính xác (main.go:11 calls yaml.Unmarshal) để biết code nào của bạn kích hoạt lỗ hổng.
Phần tổng kết thể hiện rõ giá trị lọc nhiễu:
Your code is affected by 3 vulnerabilities from 1 module.
This scan also found 3 vulnerabilities in packages you import and
42 vulnerabilities in modules you require, but your code
doesn't appear to call these vulnerabilities.
Đây là điểm mấu chốt: 3 lỗ hổng bạn gọi tới = nguy hiểm thật, phải sửa ngay; 42 lỗ hổng chỉ có mặt trong phụ thuộc nhưng code không gọi = ưu tiên thấp, không phải báo động khẩn. Một scanner thường sẽ ném cả 45+ vào mặt bạn, khiến 3 cái thật chìm nghỉm. govulncheck tách bạch để bạn tập trung đúng chỗ.

Hình 2: Đo thật — govulncheck báo 3 lỗ hổng yaml.v2 mà code gọi tới (kèm CVE, Found in v2.2.1 -> Fixed in, và trace main.go:11 calls yaml.Unmarshal), tách khỏi 42 lỗ hổng chỉ-có-trong-phụ-thuộc-không-gọi; sửa bằng go get @v2.2.8 rồi quét lại.
Sửa: nâng phiên bản lên bản đã vá
$ go get gopkg.in/yaml.v2@v2.2.8 # bản vá cả 3 CVE
$ govulncheck ./... # quét lại -> sạch
Trường Fixed in cho biết chính xác phiên bản tối thiểu vá mỗi lỗ hổng — ở đây nâng lên v2.2.8 vá cả ba (vì nó lớn hơn tất cả các mốc vá 2.2.4, 2.2.3, 2.2.8). Chạy lại govulncheck để xác nhận sạch.
Đánh đổi cần cân nhắc
Call graph có thể bỏ sót lời gọi qua reflection hoặc động. Sức mạnh của govulncheck (chỉ báo cái được gọi) cũng là giới hạn: nếu code gọi hàm lỗ hổng gián tiếp qua reflect, qua plugin nạp động, hoặc qua cơ chế mà phân tích tĩnh không lần được, govulncheck có thể không báo dù thực tế có gọi. Vì thế "govulncheck sạch" là tín hiệu tốt nhưng không phải bảo đảm tuyệt đối cho code dùng nhiều reflection. Với code như vậy, cân nhắc cả scanner theo phiên bản (báo mọi CVE) như một lớp bổ sung.
Chỉ quét lỗ hổng đã biết trong CSDL Go. govulncheck đối chiếu với vuln.go.dev — nó không tìm được lỗ hổng chưa được báo cáo, hay bug bảo mật trong chính code của bạn (đó là việc của SAST, fuzzing, review). Nó là một lớp trong phòng thủ nhiều lớp, không phải giải pháp bảo mật toàn diện. Chạy nó định kỳ (CSDL cập nhật liên tục) chứ không phải một lần.
Nên chạy trong CI, không chỉ thủ công. Lỗ hổng mới được công bố mỗi ngày, và một phụ thuộc hôm nay sạch có thể mai dính CVE mới. Tích hợp govulncheck ./... vào pipeline CI (chạy mỗi build hoặc theo lịch) để bắt lỗ hổng mới trên phụ thuộc hiện có, không đợi tới lúc nâng cấp. Vì nó lọc theo call graph, tỉ lệ cảnh báo giả thấp nên phù hợp để chặn build khi có lỗ hổng gọi-tới.
Ba ý mang về
- govulncheck phân tích call graph, chỉ báo lỗ hổng code thực sự gọi tới — khác scanner thường báo mọi phụ thuộc dính CVE: đo thật, 3 lỗ hổng yaml.v2 mà code gọi (kèm trace
main.go:11 calls yaml.Unmarshalvà phiên bản vá) so với 42 lỗ hổng chỉ-có-mặt-không-chạm, giúp không chết đuối trong cảnh báo giả. - Mỗi lỗ hổng kèm CVE, phiên bản dính/vá, và trace chính xác:
Found in v2.2.1 -> Fixed in v2.2.8cho biết nâng lên đâu, trace cho biết code nào kích hoạt — sửa bằnggo get @<bản vá>rồi quét lại. - Nêu rõ giới hạn: call graph có thể bỏ sót lời gọi qua reflection/động, chỉ quét lỗ hổng đã biết trong CSDL (không thay SAST/fuzzing), và nên chạy trong CI định kỳ vì lỗ hổng mới xuất hiện liên tục.
Phần sau ta xét lớp phân tích tĩnh rộng hơn: staticcheck và golangci-lint sâu trong Go — bắt lỗi tiềm ẩn, code mùi, và lỗi hiệu năng ngay khi viết, trước cả khi chạy.