【Go语言成长之路】 模糊测试

文章目录

模糊测试

​ 本教程介绍了 Go 中模糊测试的基础知识。通过模糊测试,随机数据会针对您的测试运行,以尝试找到漏洞或导致崩溃的输入。可以通过模糊测试发现的漏洞示例包括 SQL 注入、缓冲区溢出、拒绝服务和跨站点脚本攻击。

注:Go语言中模糊测试已经内置,具体可以参考: Go Fuzzing docs, 将来还会添加更多功能。

一、前提

  • Go1.18以及之后。
  • 支持模糊测试的环境。目前,使用覆盖率检测进行模糊测试仅适用于 AMD64 和 ARM64 架构。

二、创建项目

sh 复制代码
~$ mkdir fuzz
~$ cd fuzz/
~/fuzz$ go mod init example/fuzz
go: creating new go.mod: module example/fuzz

三、添加待测试代码

go 复制代码
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)
}

运行代码:

sh 复制代码
~/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

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)
		}
	}
}

这个简单的测试将断言列出的输入字符串将被正确反转。

之后运行测试代码:

sh 复制代码
~/fuzz$ go test . -v
=== RUN   TestReverse
--- PASS: TestReverse (0.00s)
PASS
ok      example/fuzz    0.003s

五、添加模糊测试

​ 单元测试有局限,即每个输入都必须由开发人员添加到测试中,使用模糊测试的好处就是可以为代码提供输入,并且可以识别测试用例没有达到边缘情况。

注:单元测试、基准测试和模糊测试可以保留在同一个*_test.go文件中。

​ 编写的模糊测试函数代码如下:

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 将单元测试的输入作为种子语料库输入提供。

之后就可以运行模糊测试并查看结果:

sh 复制代码
~/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

如果你只想运行模糊测试,那么可以执行如下命令:

sh 复制代码
~/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目录的语料库文件。您的种子语料库文件可能包含不同的字符串,但格式将相同, 类型如下:

sh 复制代码
go test fuzz v1
string("֣")

​ 语料库文件的第一行表示编码版本。接下来的每一行代表构成语料库条目的每种类型的值。由于模糊目标仅需要 1 个输入,因此版本后只有 1 个值。

​ 再次运行 go test,不带 -fuzz 标志;将使用新的失败种子语料库条目:

sh 复制代码
~/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

若想使用原来失败的语料种子,可以执行如下命令:

sh 复制代码
~/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 行将打印到命令行,这可以帮助您调试此特定问题。

  • 解决错误

相关推荐
ChinaRainbowSea2 分钟前
4. Spring Cloud Ribbon 实现“负载均衡”的详细配置说明
java·spring boot·后端·spring·spring cloud·ribbon·负载均衡
TPCloud14 分钟前
快速利用c语言实现线性表(lineList)
c语言·开发语言·线性表·linelist
如意.75920 分钟前
【C++】—— map 与 set 深入浅出:设计原理与应用对比
开发语言·c++
容器( ु⁎ᴗ_ᴗ⁎)ु.。oO1 小时前
java中的定时器
java·开发语言
m0_547486661 小时前
一道C语言关于距离的期末题及答案
c语言·开发语言
蹊黎1 小时前
C++模版初阶
开发语言·c++
山川尔尔_1 小时前
JS手写-this绑定实现
开发语言·javascript·ecmascript
YYDS3141 小时前
C++各类函数评点+详解
开发语言·数据结构·c++·算法·贪心算法·动态规划
薯条不要番茄酱2 小时前
数据结构-5.二叉树
java·开发语言·数据结构
YAy173 小时前
CC3学习记录
java·开发语言·学习·网络安全·安全威胁分析