本篇文章主要讲解在 go 语言中进行模糊测试的基础知识。通过模糊测试,随机数据会针对您的测试运行,以尝试找出漏洞或导致崩溃的输入。可以通过模糊测试发现的一些漏洞示例包括 SQL 注入、buffer overflow、拒绝服务和 cross-site scripting 攻击。
想要在 go 中使用模糊测试,需要安装 go1.18beta1 以上版本,具体的安装就不过多叙述了。
1 创建项目文件夹
我们创建一个名叫 fuzz-demo
的文件夹,并在其中创建一个名叫 main.go
的文件。
2 输入代码
在 main.go
中输入如下代码:
package main
import "fmt"
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 := "quick quick brown fox jumped over the lazy dog"
rev := Reverse(input)
doubleRev := Reverse(rev)
fmt.Printf("原来: %q\n", input)
fmt.Printf("反转: %q\n", rev)
fmt.Printf("再反转: %q\n", doubleRev)
}
Reverse 函数的作用就是对字符串进行反转。
3 运行代码,可以看到如下输出:
4 编写单元测试,我们为 Reverse 函数编写了一个单元测试。如下:
package main
import "testing"
func TestReverse(t *testing.T) {
type args struct {
s string
}
tests := []struct {
name string
args args
want string
}{
{"test1", args{"Hello, world"}, "dlrow ,olleH"},
{"test2", args{" "}, " "},
{"test3", args{"!12345"}, "54321!"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Reverse(tt.args.s); got != tt.want {
t.Errorf("Reverse() = %v, want %v", got, tt.want)
}
})
}
}
5 将单元测试修改为模糊测试
单元测试有局限性,即每个输入都必须由开发人员添加到测试中。模糊测试的一个好处是它可以为您的代码提供输入,并且可以识别您提出的测试用例没有达到的边缘用例。
模糊测试代码如下:
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 := 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)
}
})
}
模糊测试 也有一些限制。在您的单元测试中,您可以预测Reverse函数的预期输出,并验证实际输出是否满足这些预期。
例如,在测试用例Reverse(“Hello, world”)中,单元测试将返回指定为"dlrow ,olleH".
模糊测试时,您无法预测预期输出,因为您无法控制输入。
但是,Reverse您可以在模糊测试中验证函数的一些属性。在这个模糊测试中检查的两个属性是:
1 将字符串反转两次保留原始值
2 反转的字符串将其状态保留为有效的 UTF-8。
注意单元测试和模糊测试之间的语法差异:
该函数以 FuzzXxx 而不是 TestXxx 开头,取testing.F 而不是testing.T 在你期望看到t.Run执行的地方,你看到的是f.Fuzz,它接受一个参数为* testing的fuzz目标函数。T和需要模糊化的类型。单元测试的输入使用f.Add作为种子语料库输入提供。
6 运行测试
使用命令 go test 进行测试以保证种子正确,如果您在该文件中有其他测试,您也可以运行go test -run=FuzzReverse,并且您只想运行模糊测试。
运行 go test -fuzz=Fuzz
进行模糊测试,测试失败,具体失败信息如下:
Failing input written to testdata\fuzz\FuzzReverse\09f84a1d1fc1c0a975a2de415e883bfc189bb7d17eaad24d85b68a17fd81c8f9
To re-run:
go test -run=FuzzReverse/09f84a1d1fc1c0a975a2de415e883bfc189bb7d17eaad24d85b68a17fd81c8f9
FAIL
exit status 1
FAIL github.com/overstarry/funzz-demo 0.950s
模糊测试时发生故障,导致问题的输入被写入将在下次运行的种子语料库文件中go test,即使没有-fuzz标志也是如此。要查看导致失败的输入,请在编辑器中打开 testdata/fuzz/FuzzReverse 目录的语料库文件。
go test fuzz v1
string("扖")
语料库文件的第一行表示编码版本。以下每一行代表构成语料库条目的每种类型的值。由于 fuzz target 只需要 1 个输入,因此版本之后只有 1 个值。
不使用 -fuzz 运行测试,这次测试将会自动使用模糊测试失败的语料.
7 修改函数的错误
接下来对代码进行修复以通过测试,修复后的代码如下:
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)
}
})
}
再次运行模糊测试,可以看到所有的测试都通过了。
小结
本篇文章主要介绍在 go 中如何进行模糊测试。完整代码: https://github.com/overstarry/fuzz-demo