本教程介绍了 Go 中模糊测试(fuzzing)的基础知识。通过模糊测试,随机数据会作为输入运行你的测试,以试图找到漏洞或导致崩溃的输入。模糊测试可以发现的漏洞示例包括 SQL 注入、缓冲区溢出、拒绝服务和跨站脚本攻击。
在本教程中,你将为一个简单的函数编写模糊测试,运行 go 命令,并调试和修复代码中的问题。
前置条件
- 安装 Go 1.18 或更高版本。
- 支持模糊测试的环境。Go 模糊测试目前仅在 AMD64 和 ARM64 架构上支持覆盖率插桩。
添加待测试代码
我们将添加一个反转字符串的函数,稍后对其进行模糊测试。
编写代码
在 fuzz 目录下创建一个名为 main.go 的文件。
package main
import "fmt"
func main() {
input := "The quick brown fox jumped over the lazy dog"
rev := Reverse(input)
doubleRev := Reverse(rev)
fmt.Printf("original: %q\n", input)
fmt.Printf("reversed: %q\n", rev)
fmt.Printf("reversed again: %q\n", doubleRev)
}
func Reverse(s string) string {
b := []byte(s)
for i, j := 0, len(b)-1; i < len(b)/2; i, j = i+1, j-1 {
b[i], b[j] = b[j], b[i]
}
return string(b)
}运行代码
go run . 输出应如下所示:
original: "The quick brown fox jumped over the lazy dog"
reversed: "god yzal eht revo depmuj xof nworb kciuq ehT"
reversed again: "The quick brown fox jumped over the lazy dog"添加单元测试
编写代码
在 fuzz 目录下创建一个名为 reverse_test.go 的文件。
package main
import (
"testing"
)
func TestReverse(t *testing.T) {
testcases := []struct {
in, want string
}{
{"Hello, world", "dlrow ,olleH"},
{" ", " "},
{"!12345", "54321!"},
}
for _, tc := range testcases {
rev := Reverse(tc.in)
if rev != tc.want {
t.Errorf("Reverse: %q, want %q", rev, tc.want)
}
}
}运行代码
go test 输出应为 PASS。
添加模糊测试
单元测试的局限性在于开发者必须手动添加每个输入。模糊测试的好处是它可以为你的代码生成输入,并可能识别出你未考虑到的边缘情况。
编写代码
将 reverse_test.go 中的单元测试替换为以下模糊测试:
package main
import (
"testing"
"unicode/utf8"
)
func FuzzReverse(f *testing.F) {
testcases := []string{"Hello, world", " ", "!12345"}
for _, tc := range testcases {
f.Add(tc) // 使用 f.Add 提供种子语料库
}
f.Fuzz(func(t *testing.T, orig string) {
rev := Reverse(orig)
doubleRev := Reverse(rev)
if orig != doubleRev {
t.Errorf("Before: %q, after: %q", orig, doubleRev)
}
if utf8.ValidString(orig) && !utf8.ValidString(rev) {
t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
}
})
}注意模糊测试与单元测试的区别:
- 函数名以
FuzzXxx开头,参数为*testing.F。 - 使用
f.Add提供种子语料库(seed corpus)。 - 使用
f.Fuzz执行模糊测试目标函数。
运行代码
-
运行模糊测试(不带 fuzzing),确保种子输入通过:
go test -
运行模糊测试:
go test -fuzz=Fuzz你可能会看到类似以下的失败信息:
fuzz: minimizing 38-byte failing input file... --- FAIL: FuzzReverse (0.01s) --- FAIL: FuzzReverse (0.00s) reverse_test.go:20: Reverse produced invalid UTF-8 string "\x9c\xdd"这表明模糊测试发现了一个导致失败的输入。
修复无效字符串错误
诊断错误
Reverse 函数按字节反转字符串。如果输入包含多字节字符(如中文),按字节反转会破坏 UTF-8 编码。
修复代码
修改 main.go 中的 Reverse 函数,使其按 rune(字符)遍历:
func Reverse(s string) string {
r := []rune(s)
for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return string(r)
}再次运行
- 运行测试
go test测试应该通过。 - 再次运行模糊测试
go test -fuzz=Fuzz这次可能会发现另一个错误:
--- FAIL: FuzzReverse (0.02s)
--- FAIL: FuzzReverse (0.00s)
reverse_test.go:33: Before: "\x91", after: ""修复双重反转错误
诊断错误
输入字符串可能包含无效的 UTF-8 字节。当转换为 []rune 时,Go 会将无效字节替换为替换字符 “(U+FFFD)。因此,反转后再反转回来的字符串与原始字符串不匹配。
修复代码
修改 Reverse 函数,如果输入不是有效的 UTF-8,则返回错误。
- 修改
main.go:
package main
import (
"errors"
"fmt"
"unicode/utf8"
)
func main() {
input := "The quick brown fox jumped over the lazy dog"
rev, revErr := Reverse(input)
doubleRev, doubleRevErr := Reverse(rev)
fmt.Printf("original: %q\n", input)
fmt.Printf("reversed: %q, err: %v\n", rev, revErr)
fmt.Printf("reversed again: %q, err: %v\n", doubleRev, doubleRevErr)
}
func Reverse(s string) (string, error) {
if !utf8.ValidString(s) {
return s, errors.New("input is not valid UTF-8")
}
r := []rune(s)
for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return string(r), nil
}- 修改
reverse_test.go以处理错误:
package main
import (
"testing"
"unicode/utf8"
)
func FuzzReverse(f *testing.F) {
testcases := []string{"Hello, world", " ", "!12345"}
for _, tc := range testcases {
f.Add(tc)
}
f.Fuzz(func(t *testing.T, orig string) {
rev, err1 := Reverse(orig)
if err1 != nil {
return
}
doubleRev, err2 := Reverse(rev)
if err2 != nil {
return
}
if orig != doubleRev {
t.Errorf("Before: %q, after: %q", orig, doubleRev)
}
if utf8.ValidString(orig) && !utf8.ValidString(rev) {
t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
}
})
}最终运行
运行模糊测试一段时间(例如 30 秒):
go test -fuzz=Fuzz -fuzztime 30s应该会显示 PASS。
总结
你已经完成了 Go 模糊测试的入门教程!模糊测试可以帮助你发现单元测试难以覆盖的边缘情况和漏洞。
完整代码
package main
import (
"errors"
"fmt"
"unicode/utf8"
)
func main() {
input := "The quick brown fox jumped over the lazy dog"
rev, revErr := Reverse(input)
doubleRev, doubleRevErr := Reverse(rev)
fmt.Printf("original: %q\n", input)
fmt.Printf("reversed: %q, err: %v\n", rev, revErr)
fmt.Printf("reversed again: %q, err: %v\n", doubleRev, doubleRevErr)
}
func Reverse(s string) (string, error) {
if !utf8.ValidString(s) {
return s, errors.New("input is not valid UTF-8")
}
r := []rune(s)
for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return string(r), nil
}package main
import (
"testing"
"unicode/utf8"
)
func FuzzReverse(f *testing.F) {
testcases := []string{"Hello, world", " ", "!12345"}
for _, tc := range testcases {
f.Add(tc)
}
f.Fuzz(func(t *testing.T, orig string) {
rev, err1 := Reverse(orig)
if err1 != nil {
return
}
doubleRev, err2 := Reverse(rev)
if err2 != nil {
return
}
if orig != doubleRev {
t.Errorf("Before: %q, after: %q", orig, doubleRev)
}
if utf8.ValidString(orig) && !utf8.ValidString(rev) {
t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
}
})
}