模糊测试
本教程介绍了 Go 中模糊测试的基础知识。通过模糊测试,随机数据会针对您的测试运行,以尝试找到漏洞或导致崩溃的输入。可以通过模糊测试发现的漏洞示例包括 SQL 注入、缓冲区溢出、拒绝服务和跨站点脚本攻击。
注:Go语言中模糊测试已经内置,具体可以参考: Go Fuzzing docs, 将来还会添加更多功能。
一、前提
- Go1.18以及之后。
- 支持模糊测试的环境。目前,使用覆盖率检测进行模糊测试仅适用于 AMD64 和 ARM64 架构。
二、创建项目
~$ mkdir fuzz
~$ cd fuzz/
~/fuzz$ go mod init example/fuzz
go: creating new go.mod: module example/fuzz
三、添加待测试代码
package main
import "fmt"
// accept a string, loop over it a byte at a time, and return the reversed string at the end.
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)
}
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)
}
运行代码:
~/fuzz$ go run ./fuzz
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"
四、添加单元测试
为 Reverse 函数编写基本单元测试,在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)
}
}
}
这个简单的测试将断言列出的输入字符串将被正确反转。
之后运行测试代码:
~/fuzz$ go test . -v
=== RUN TestReverse
--- PASS: TestReverse (0.00s)
PASS
ok example/fuzz 0.003s
五、添加模糊测试
单元测试有局限,即每个输入都必须由开发人员添加到测试中,使用模糊测试的好处就是可以为代码提供输入,并且可以识别测试用例没有达到边缘情况。
注:单元测试、基准测试和模糊测试可以保留在同一个*_test.go
文件中。
编写的模糊测试函数代码如下:
func FuzzReverse(f *testing.F) {
testcases := []string{"Hello, world", " ", "!12345"}
for _, tc := range testcases {
f.Add(tc) // Use f.Add to provide a seed corpus
}
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)
}
})
}
使用模糊测试有如下几个特点:
-
无法预测结果。因为无法控制模糊测试的输入
-
可以验证函数的属性,在本例中可以检查的属性有:
- 将字符串反转两次可以保留原始值
- 反转的字符串将其状态保留为有效的 UTF-8。
请注意单元测试和模糊测试之间的语法差异:
- 该函数以
FuzzXxx
而不是TestXxx
开头,并采用*testing.F
而不是*testing.T
- 将 t.Run 替换为 f.Fuzz,它采用了一个模糊目标函数,其参数为 *testing.T 和要模糊的类型。使用
f.Add
将单元测试的输入作为种子语料库输入提供。
之后就可以运行模糊测试并查看结果:
~/fuzz$ go test ./fuzz -v
=== RUN TestReverse
--- PASS: TestReverse (0.00s)
=== RUN FuzzReverse
=== RUN FuzzReverse/seed#0
=== RUN FuzzReverse/seed#1
=== RUN FuzzReverse/seed#2
--- PASS: FuzzReverse (0.00s)
--- PASS: FuzzReverse/seed#0 (0.00s)
--- PASS: FuzzReverse/seed#1 (0.00s)
--- PASS: FuzzReverse/seed#2 (0.00s)
PASS
ok example/fuzz 0.003s
如果你只想运行模糊测试,那么可以执行如下命令:
~/fuzz$ go test . -run=FuzzReverse -v # 运行特定的函数
=== RUN FuzzReverse
=== RUN FuzzReverse/seed#0
=== RUN FuzzReverse/seed#1
=== RUN FuzzReverse/seed#2
--- PASS: FuzzReverse (0.00s)
--- PASS: FuzzReverse/seed#0 (0.00s)
--- PASS: FuzzReverse/seed#1 (0.00s)
--- PASS: FuzzReverse/seed#2 (0.00s)
PASS
ok example/fuzz 0.003s
~/fuzz$go test -fuzz=Fuzz . -v # 只运行模糊测试
=== RUN TestReverse
--- PASS: TestReverse (0.00s)
=== RUN FuzzReverse
fuzz: elapsed: 0s, gathering baseline coverage: 0/3 completed
fuzz: elapsed: 0s, gathering baseline coverage: 3/3 completed, now fuzzing with 8 workers
fuzz: elapsed: 0s, execs: 253 (10168/sec), new interesting: 2 (total: 5)
--- FAIL: FuzzReverse (0.03s)
--- FAIL: FuzzReverse (0.00s)
reverse_test.go:36: Reverse produced invalid UTF-8 string "\xa3\xd6"
Failing input written to testdata/fuzz/FuzzReverse/14a85158d50021f3
To re-run:
go test -run=FuzzReverse/14a85158d50021f3
=== NAME
FAIL
exit status 1
FAIL example/fuzz 0.029s
另一个有用的标志是 -fuzztime,它限制模糊测试所需的时间, 若没有指定模糊测试的运行时间,那么它将会一直运行下去,直到发生错误。
模糊测试时发生故障,导致问题的输入被写入种子语料库文件
,该文件将在下次调用 go test 时运行,即使没有 -fuzz 标志也是如此。要查看导致失败的输入,请在文本编辑器中打开写入 testdata/fuzz/FuzzReverse
目录的语料库文件。您的种子语料库文件可能包含不同的字符串,但格式将相同, 类型如下:
go test fuzz v1
string("֣")
语料库文件的第一行表示编码版本。接下来的每一行代表构成语料库条目的每种类型的值。由于模糊目标仅需要 1 个输入,因此版本后只有 1 个值。
再次运行 go test,不带 -fuzz 标志;将使用新的失败种子语料库条目:
~/fuzz$ go test ./fuzz
--- FAIL: FuzzReverse (0.00s)
--- FAIL: FuzzReverse/14a85158d50021f3 (0.00s)
reverse_test.go:36: Reverse produced invalid UTF-8 string "\xa3\xd6"
FAIL
FAIL example/fuzz 0.003s
FAIL
若想使用原来失败的语料种子,可以执行如下命令:
~/fuzz$ go test -run=FuzzReverse/28f36ef487f23e6c7a81ebdaa9feffe2f2b02b4cddaa6252e87f69863046a5e0
input: "\x91"
runes: ['�']
input: "�"
runes: ['�']
--- FAIL: FuzzReverse (0.00s)
--- FAIL: FuzzReverse/28f36ef487f23e6c7a81ebdaa9feffe2f2b02b4cddaa6252e87f69863046a5e0 (0.00s)
reverse_test.go:16: Number of runes: orig=1, rev=1, doubleRev=1
reverse_test.go:18: Before: "\x91", after: "�"
FAIL
exit status 1
FAIL example/fuzz 0.145s
之后就是定位问题并解决了。
遇到错误的几点建议:
-
诊断错误
有如下几种方法:
-
通过VS Code设置调试器进行调查(遇到比较棘手的错误可以使用此方法)
-
将把有用的调试信息记录到您的终端
比如说使用
t.LogF
将错误相关的内容打印出来,或者如果发生错误,或者使用 -v 执行测试,此t.Logf
行将打印到命令行,这可以帮助您调试此特定问题。
-
-
解决错误