☰ Mục lục series
Tài liệu Go tiếng Việt
  1. Bắt đầu (Getting started)
    1. Tổng quan
    2. Tải về và cài đặt
    3. Bắt đầu với Go
    4. Quản lý các bản cài đặt Go
    5. Plugin trình soạn thảo và IDE
  2. Dạo quanh Go (A Tour of Go)
    1. Chào mừng
    2. Package, biến và hàm
    3. Điều khiển luồng
    4. Struct, slice và map
    5. Method và interface
    6. Generics
    7. Xử lý đồng thời
  3. Hướng dẫn (Tutorials)
    1. Danh sách hướng dẫn
    2. Tạo một Go module
    3. Gọi mã của bạn từ một module khác
    4. Trả về và xử lý lỗi
    5. Trả về lời chào ngẫu nhiên
    6. Trả về lời chào cho nhiều người
    7. Thêm một test
    8. Biên dịch và cài đặt ứng dụng
    9. Kết luận
    10. Bắt đầu với workspace nhiều module
    11. Phát triển RESTful API với Go và Gin
    12. Truy cập cơ sở dữ liệu quan hệ
    13. Bắt đầu với generics
    14. Bắt đầu với fuzzing
    15. Làm việc với JSON
    16. Tìm và sửa các dependency có lỗ hổng với govulncheck
    17. Tìm và khắc phục phụ thuộc có lỗ hổng với VS Code Go
    18. Viết ứng dụng web
  4. Viết code Go
    1. Cách viết mã Go
    2. Effective Go
    3. Doc Comment trong Go
  5. Module
    1. Phát triển và publish module
    2. Quản lý phụ thuộc
    3. Tổ chức một module Go
    4. Quản lý mã nguồn của module
    5. Cách đánh số phiên bản module
    6. Quy trình phát hành và đánh phiên bản module
    7. Publish một module
    8. Phát triển một bản cập nhật major version
    9. Tham chiếu tệp go.mod
    10. Ngừng hỗ trợ 'go get' cho việc cài đặt tệp thực thi
  6. Cơ sở dữ liệu (Database)
    1. Truy cập cơ sở dữ liệu quan hệ
    2. Mở một handle cơ sở dữ liệu
    3. Thực thi câu lệnh SQL không trả về dữ liệu
    4. Truy vấn dữ liệu
    5. Dùng prepared statement
    6. Thực thi transaction
    7. Huỷ các thao tác đang chạy
    8. Quản lý kết nối
    9. Tránh rủi ro SQL injection
  7. Chẩn đoán và hiệu năng
    1. Chẩn đoán
    2. Hướng dẫn về Go Garbage Collector
    3. Tối ưu hoá dựa trên profile
    4. Data Race Detector
    5. Gỡ lỗi mã Go bằng GDB
    6. Hỗ trợ coverage profiling cho integration test
  8. Bảo mật (Security)
    1. Tổng quan bảo mật
    2. Thực hành tốt về bảo mật cho lập trình viên Go
    3. Quản lý lỗ hổng trong Go
    4. Cơ sở dữ liệu lỗ hổng của Go
    5. Quét lỗ hổng trong IDE
    6. Chính sách CNA của Go
    7. Go Fuzzing
    8. Chi tiết kỹ thuật của Go Fuzzing
    9. Chính sách bảo mật của Go
    10. Threat Model của Go
    11. Các quyết định về bảo mật của Go
    12. Tuân thủ FIPS 140-3
  9. Công cụ và toolchain
    1. Tài liệu về các lệnh
    2. Về lệnh go
    3. Go Toolchains
    4. Telemetry của Go
    5. Cài đặt Go từ mã nguồn
    6. Cài đặt và sử dụng gccgo
  10. Tham khảo
    1. Câu hỏi thường gặp (FAQ)
    2. Go 1 và tương lai của các chương trình Go
    3. Hướng dẫn chuyển đổi sang encoding/json/v2
    4. Hướng dẫn đóng góp
    5. Đóng góp cho frontend gccgo
  11. Phụ lục
    1. Bảng thuật ngữ
Bảo mật (Security)

Go Fuzzing

Bản dịch từ go.dev/doc/security/fuzz/

Go hỗ trợ fuzzing (kỹ thuật sinh dữ liệu đầu vào ngẫu nhiên để dò lỗi) trong toolchain chuẩn kể từ Go 1.18. Fuzz test viết bằng Go thuần được OSS-Fuzz hỗ trợ.

Hãy thử bài hướng dẫn fuzzing với Go.

Tổng quan

Fuzzing là một dạng kiểm thử tự động, liên tục biến đổi input của một chương trình để tìm lỗi. Fuzzing trong Go dùng coverage guidance để đi xuyên qua mã nguồn đang được fuzz một cách thông minh, nhằm tìm ra và report các thất bại cho người dùng. Vì nó có thể chạm tới những trường hợp biên mà con người thường bỏ sót, fuzz test đặc biệt hữu ích để tìm ra các lỗ hổng và cách khai thác về bảo mật.

Dưới đây là ví dụ về một fuzz test, nêu bật những thành phần chính của nó.

Mã ví dụ của một fuzz test: fuzz target nằm bên trong, phía trước là lệnh f.Add thêm dữ liệu vào corpus, tham số của fuzz target được đánh dấu là fuzzing argument

Viết fuzz test

Yêu cầu

Dưới đây là các quy tắc mà fuzz test phải tuân theo.

  • Một fuzz test phải là một hàm được đặt tên dạng FuzzXxx, chỉ nhận một *testing.F, và không có giá trị trả về.
  • Fuzz test phải nằm trong các tệp *_test.go thì mới chạy được.
  • Một fuzz target phải là một lời gọi method tới (*testing.F).Fuzz, nhận một *testing.T làm tham số đầu tiên, tiếp theo là các đối số fuzzing. Không có giá trị trả về.
  • Mỗi fuzz test phải có đúng một fuzz target.
  • Mọi mục trong seed corpus phải có kiểu dữ liệu giống hệt các đối số fuzzing, theo cùng thứ tự. Điều này đúng cho cả các lời gọi (*testing.F).Add lẫn mọi tệp corpus trong thư mục testdata/fuzz của fuzz test đó.
  • Các đối số fuzzing chỉ có thể thuộc những kiểu sau:
    • string, []byte
    • int, int8, int16, int32/rune, int64
    • uint, uint8/byte, uint16, uint32, uint64
    • float32, float64
    • bool

Gợi ý

Dưới đây là những gợi ý giúp bạn tận dụng tối đa fuzzing.

  • Fuzz target nên nhanh và tất định để fuzzing engine làm việc hiệu quả, đồng thời các thất bại mới và code coverage có thể dễ dàng tái hiện.
  • Vì fuzz target được gọi song song trên nhiều worker và theo thứ tự không tất định, trạng thái của một fuzz target không nên tồn tại quá thời điểm kết thúc mỗi lời gọi, và hành vi của một fuzz target không nên phụ thuộc vào trạng thái toàn cục.

Chạy fuzz test

Có hai chế độ chạy fuzz test: như một unit test (mặc định go test), hoặc kèm fuzzing (go test -fuzz=FuzzTestName).

Mặc định, fuzz test được chạy khá giống một unit test. Mỗi mục trong seed corpus sẽ được kiểm thử với fuzz target, và mọi thất bại sẽ được report trước khi thoát.

Để bật fuzzing, hãy chạy go test với cờ -fuzz, cung cấp một biểu thức chính quy khớp với đúng một fuzz test. Mặc định, mọi test khác trong package đó sẽ chạy trước khi fuzzing bắt đầu. Việc này nhằm đảm bảo fuzzing sẽ không report những vấn đề mà một test sẵn có đã bắt được.

Lưu ý rằng bạn là người quyết định chạy fuzzing trong bao lâu. Rất có thể một lần chạy fuzzing sẽ chạy mãi không dừng nếu nó không tìm thấy lỗi nào. Trong tương lai sẽ có hỗ trợ chạy các fuzz test này liên tục bằng những công cụ như OSS-Fuzz, xem Issue #50192.

Lưu ý: Nên chạy fuzzing trên nền tảng hỗ trợ coverage instrumentation (hiện tại là AMD64 và ARM64) để corpus có thể lớn dần một cách có ý nghĩa trong lúc chạy, và nhiều mã nguồn hơn được bao phủ khi fuzzing.

Đầu ra dòng lệnh

Trong lúc fuzzing đang diễn ra, fuzzing engine sinh ra các input mới và chạy chúng với fuzz target được cung cấp. Mặc định, nó tiếp tục chạy cho tới khi tìm thấy một input gây thất bại, hoặc người dùng hủy tiến trình (ví dụ bằng Ctrl^C).

Đầu ra sẽ trông đại khái như sau:

~ go test -fuzz FuzzFoo
fuzz: elapsed: 0s, gathering baseline coverage: 0/192 completed
fuzz: elapsed: 0s, gathering baseline coverage: 192/192 completed, now fuzzing with 8 workers
fuzz: elapsed: 3s, execs: 325017 (108336/sec), new interesting: 11 (total: 202)
fuzz: elapsed: 6s, execs: 680218 (118402/sec), new interesting: 12 (total: 203)
fuzz: elapsed: 9s, execs: 1039901 (119895/sec), new interesting: 19 (total: 210)
fuzz: elapsed: 12s, execs: 1386684 (115594/sec), new interesting: 21 (total: 212)
PASS
ok      foo 12.692s

Những dòng đầu tiên cho biết “baseline coverage” được thu thập trước khi fuzzing bắt đầu.

Để thu thập baseline coverage, fuzzing engine thực thi cả seed corpus lẫn generated corpus, nhằm đảm bảo không có lỗi nào xảy ra và để nắm được code coverage mà corpus hiện có đã cung cấp.

Các dòng tiếp theo cho bạn cái nhìn về quá trình fuzzing đang chạy:

  • elapsed: khoảng thời gian đã trôi qua kể từ khi tiến trình bắt đầu
  • execs: tổng số input đã được chạy với fuzz target (kèm số execs/giây trung bình kể từ dòng log gần nhất)
  • new interesting: tổng số input “thú vị” đã được thêm vào generated corpus trong lần chạy fuzzing này (kèm tổng kích thước của toàn bộ corpus)

Để một input được coi là “thú vị”, nó phải mở rộng code coverage vượt quá phạm vi mà generated corpus hiện có chạm tới được. Thông thường số lượng input thú vị mới sẽ tăng nhanh lúc đầu rồi dần chậm lại, thỉnh thoảng bùng lên khi phát hiện nhánh mới.

Bạn nên trông đợi con số “new interesting” giảm dần theo thời gian khi các input trong corpus bắt đầu bao phủ nhiều dòng mã hơn, thỉnh thoảng bùng lên nếu fuzzing engine tìm thấy một đường đi mới trong mã.

Input gây thất bại

Một thất bại có thể xảy ra trong lúc fuzzing vì nhiều lý do:

  • Một panic xảy ra trong mã nguồn hoặc trong test.
  • Fuzz target đã gọi t.Fail, trực tiếp hoặc thông qua các method như t.Error hay t.Fatal.
  • Một lỗi không thể khôi phục đã xảy ra, chẳng hạn os.Exit hoặc tràn stack.
  • Fuzz target mất quá nhiều thời gian để hoàn thành. Hiện tại, thời gian chờ cho một lần thực thi fuzz target là 1 giây. Điều này có thể thất bại do deadlock hoặc vòng lặp vô hạn, hoặc do hành vi cố ý trong mã nguồn. Đây là một lý do vì sao khuyến nghị rằng fuzz target của bạn nên nhanh.

Nếu có lỗi xảy ra, fuzzing engine sẽ cố gắng thu nhỏ input xuống giá trị nhỏ nhất có thể và dễ đọc nhất với con người mà vẫn tạo ra lỗi đó. Để cấu hình việc này, xem phần thiết lập tùy chỉnh.

Khi quá trình thu nhỏ hoàn tất, thông báo lỗi sẽ được ghi log, và đầu ra sẽ kết thúc với nội dung đại khái như sau:

    Failing input written to testdata/fuzz/FuzzFoo/a878c3134fe0404d44eb1e662e5d8d4a24beb05c3d68354903670ff65513ff49
    To re-run:
    go test -run=FuzzFoo/a878c3134fe0404d44eb1e662e5d8d4a24beb05c3d68354903670ff65513ff49
FAIL
exit status 1
FAIL    foo 0.839s

Fuzzing engine đã ghi input gây thất bại này vào seed corpus của fuzz test đó, và từ giờ nó sẽ được chạy mặc định với go test, đóng vai trò một test hồi quy sau khi lỗi đã được khắc phục.

Bước tiếp theo của bạn là chẩn đoán vấn đề, sửa lỗi, kiểm chứng bản sửa bằng cách chạy lại go test, rồi gửi bản vá kèm tệp testdata mới đóng vai trò test hồi quy của bạn.

Thiết lập tùy chỉnh

Các thiết lập mặc định của lệnh go sẽ phù hợp với hầu hết trường hợp dùng fuzzing. Nên thông thường, một lần chạy fuzzing trên dòng lệnh sẽ trông như sau:

$ go test -fuzz={FuzzTestName}

Tuy nhiên, lệnh go có cung cấp một vài thiết lập khi chạy fuzzing. Chúng được ghi trong tài liệu package cmd/go.

Xin nêu bật một vài thiết lập:

  • -fuzztime: tổng thời gian hoặc số lần lặp mà fuzz target sẽ được thực thi trước khi thoát, mặc định là không giới hạn.
  • -fuzzminimizetime: thời gian hoặc số lần lặp mà fuzz target sẽ được thực thi trong mỗi lần thử thu nhỏ, mặc định 60 giây. Bạn có thể tắt hoàn toàn việc thu nhỏ bằng cách đặt -fuzzminimizetime 0 khi fuzzing.
  • -parallel: số tiến trình fuzzing chạy cùng lúc, mặc định $GOMAXPROCS. Hiện tại, việc đặt -cpu khi fuzzing không có tác dụng.

Định dạng tệp corpus

Các tệp corpus được mã hóa theo một định dạng đặc biệt. Đây cũng là định dạng dùng cho cả seed corpus lẫn generated corpus.

Dưới đây là ví dụ về một tệp corpus:

go test fuzz v1
[]byte("hello\\xbd\\xb2=\\xbc ⌘")
int64(572293)

Dòng đầu tiên dùng để báo cho fuzzing engine biết phiên bản mã hóa của tệp. Dù hiện tại chưa có kế hoạch nào cho các phiên bản định dạng mã hóa trong tương lai, thiết kế vẫn phải hỗ trợ khả năng đó.

Mỗi dòng tiếp theo là các giá trị tạo nên mục corpus, và có thể được sao chép trực tiếp vào mã Go nếu bạn muốn.

Trong ví dụ trên, chúng ta có một []byte theo sau là một int64. Những kiểu này phải khớp chính xác với các đối số fuzzing, theo đúng thứ tự đó. Một fuzz target cho các kiểu này sẽ trông như sau:

f.Fuzz(func(*testing.T, []byte, int64) {})

Cách dễ nhất để chỉ định các giá trị seed corpus của riêng bạn là dùng method (*testing.F).Add. Trong ví dụ trên, nó sẽ trông như sau:

f.Add([]byte("hello\\xbd\\xb2=\\xbc ⌘"), int64(572293))

Tuy nhiên, bạn có thể có những tệp nhị phân lớn mà bạn không muốn sao chép thành mã trong test của mình, mà muốn giữ chúng như từng mục seed corpus riêng lẻ trong thư mục testdata/fuzz/{FuzzTestName}. Công cụ file2fuzz tại golang.org/x/tools/cmd/file2fuzz có thể dùng để chuyển các tệp nhị phân này thành tệp corpus được mã hóa cho []byte.

Để dùng công cụ này:

$ go install golang.org/x/tools/cmd/file2fuzz@latest
$ file2fuzz -h

Tài nguyên

  • Hướng dẫn:
  • Tài liệu:
    • Tài liệu package testing mô tả kiểu testing.F được dùng khi viết fuzz test.
    • Tài liệu package cmd/go mô tả các cờ liên quan tới fuzzing.
  • Chi tiết kỹ thuật:

Bảng thuật ngữ

corpus entry: Một input trong corpus có thể được dùng khi fuzzing. Nó có thể là một tệp có định dạng đặc biệt, hoặc một lời gọi tới (*testing.F).Add.

coverage guidance: Một phương pháp fuzzing dùng mức mở rộng code coverage để quyết định những corpus entry nào đáng giữ lại để dùng về sau.

failing input (input gây thất bại): Input gây thất bại là một corpus entry sẽ gây ra lỗi hoặc panic khi chạy với fuzz target.

fuzz target: Hàm của fuzz test được thực thi với các corpus entry và các giá trị được sinh ra trong lúc fuzzing. Nó được cung cấp cho fuzz test bằng cách truyền hàm đó vào (*testing.F).Fuzz.

fuzz test: Một hàm trong tệp test có dạng func FuzzXxx(*testing.F) có thể dùng để fuzzing.

fuzzing: Một dạng kiểm thử tự động liên tục biến đổi input của một chương trình để tìm ra các vấn đề như lỗi hoặc lỗ hổng mà mã nguồn có thể mắc phải.

fuzzing arguments (đối số fuzzing): Các kiểu dữ liệu sẽ được truyền vào fuzz target, và bị biến đổi bởi mutator.

fuzzing engine: Một công cụ quản lý việc fuzzing, bao gồm duy trì corpus, gọi mutator, xác định coverage mới, và report các thất bại.

generated corpus: Một corpus được fuzzing engine duy trì theo thời gian trong lúc fuzzing để theo dõi tiến độ. Nó được lưu ở $GOCACHE/fuzz. Các mục này chỉ được dùng khi fuzzing.

mutator: Một công cụ dùng khi fuzzing, biến đổi ngẫu nhiên các corpus entry trước khi truyền chúng vào fuzz target.

package: Một tập hợp các tệp mã nguồn trong cùng một thư mục được biên dịch cùng nhau. Xem phần Packages trong Đặc tả Ngôn ngữ Go.

seed corpus: Một corpus do người dùng cung cấp cho fuzz test, có thể dùng để dẫn hướng fuzzing engine. Nó gồm các corpus entry được cung cấp bởi các lời gọi f.Add bên trong fuzz test, và các tệp trong thư mục testdata/fuzz/{FuzzTestName} bên trong package. Các mục này được chạy mặc định với go test, dù có fuzzing hay không.

test file (tệp test): Một tệp có định dạng xxx_test.go, có thể chứa test, benchmark, ví dụ và fuzz test.

vulnerability (lỗ hổng): Một điểm yếu nhạy cảm về bảo mật trong mã nguồn, có thể bị kẻ tấn công khai thác.

Phản hồi

Nếu bạn gặp vấn đề hoặc có ý tưởng cho một tính năng, vui lòng gửi issue.

Để thảo luận và góp ý chung về tính năng này, bạn cũng có thể tham gia kênh #fuzzing trong Gophers Slack.

Nội dung gốc: Go Documentation © The Go Authors, giấy phép CC BY 3.0. Bản dịch và ghi chú: Lê Văn Dũng.