Go语言 HTTP 服务模糊测试教程

server/2024/11/19 5:03:45/
http://www.w3.org/2000/svg" style="display: none;">

写在前面: 此博客内容已经同步到我的博客网站,如需要获得更优的阅读体验请前往https://blog.mainjay.cloudns.ch/blog/go/fuzzing-test

作为开发人员,我们并不总能预见到程序或函数可能接收到的所有可能输入。

即使我们可以定义主要的边界情况,但仍然无法预测程序在面对一些奇怪的意外输入时会如何表现。换句话说,我们通常只能发现预期会出现的bug。

这就是模糊测试(Fuzzing)派上用场的地方。在本教程中,你将学习如何在 Go 中进行模糊测试。

什么是模糊测试?

模糊测试是一种自动化的软件测试技术,它涉及向计算机程序输入大量有效的、接近有效的或无效的随机数据,并观察其行为和输出。模糊测试的目标是揭示通过传统测试方法可能无法发现的bug、崩溃和安全漏洞。

这段Go代码在正常情况下运行良好,除非你提供某些特定输入:

func Equal(a []byte, b []byte) bool {for i := range a {// 可能会因运行时错误而崩溃:索引超出范围if a[i] != b[i] {return false}}return true
}

这个示例函数在两个切片长度相等时可以完美工作。但当第一个切片比第二个长时,它会发生崩溃(索引超出范围错误)。此外,当第二个切片是第一个切片的子集时,它也不会返回正确的结果。

模糊测试技术通过用各种输入轰炸这个函数,可以轻松发现这个bug。

将模糊测试集成到团队的软件开发生命周期(SDLC)中也是一个很好的实践。例如,微软在其SDLC中使用模糊测试作为阶段之一,以发现潜在的bug和漏洞。

Go中的模糊测试

虽然已经有许多模糊测试工具存在很长时间了(例如 oss-fuzz),但自 Go 1.18 开始,模糊测试被添加到了Go的标准库中。现在它作为常规测试包的一部分,因为它是测试的一种。你还可以将它与其他测试原语一起使用,这很方便。

在Go中创建模糊测试的步骤如下:

  1. _test.go 文件中,创建一个以 Fuzz 开头并接受 *testing.F 的函数
  2. 使用 f.Add() 添加语料库种子,让模糊测试器基于它生成数据
  3. 使用 f.Fuzz() 调用模糊测试目标,传递我们的目标函数接受的模糊测试参数
  4. 使用常规的 go test 命令启动模糊测试器,但要加上 --fuzz=Fuzz 标志

注意,模糊测试参数只能是以下类型:

- string, byte, []byte
- int, int8, int16, int32/rune, int64
- uint, uint8, uint16, uint32, uint64
- float32, float64
- bool

上面Equal函数的简单模糊测试可能如下所示:

// 模糊测试
func FuzzEqual(f *testing.F) {// 添加种子语料库f.Add([]byte{'f', 'u', 'z', 'z'}, []byte{'t', 'e', 's', 't'})// 带有模糊测试参数的模糊测试目标f.Fuzz(func(t *testing.T, a []byte, b []byte) {// 调用我们的目标函数并传递模糊测试参数Equal(a, b)})
}

默认情况下,模糊测试会永远运行,所以你要么需要指定时间限制,要么等待模糊测试失败。你可以使用 --fuzz 参数指定要运行的测试。

go test --fuzz=Fuzz -fuzztime=10s

如果执行过程中出现任何错误,输出应该类似于这样:

go test --fuzz=Fuzz -fuzztime=30s
--- FAIL: FuzzEqual (0.02s)--- FAIL: FuzzEqual (0.00s)testing.go:1591: panic: runtime error: index out of rangeFailing input written to testdata/fuzz/FuzzEqual/84ed65595ad05a58To re-run:go test -run=FuzzEqual/84ed65595ad05a58

注意,导致模糊测试失败的输入被写入 testdata 文件夹中的文件,可以使用该输入标识符重新运行:

go test -run=FuzzEqual/84ed65595ad05a58

testdata 文件夹可以提交到代码库中,并用于常规测试,因为模糊测试在没有 --fuzz 标志的情况下也可以作为常规测试运行。

HTTP服务的模糊测试

通过为你的 HandlerFunc 编写测试并使用 httptest 包,也可以对 HTTP 服务进行模糊测试。如果你需要测试整个 HTTP 服务而不仅仅是底层函数,这会非常有用。

让我们现在介绍一个更真实的示例,比如一个在请求体中接受用户输入的 HTTP Handler,然后为它编写模糊测试。

我们的处理程序接受一个带有 limitoffset 字段的 JSON 请求,用于对一些静态模拟数据进行分页。让我们先定义类型:

type Request struct {Limit  int `json:"limit"`Offset int `json:"offset"`
}type Response struct {Results    []int `json:"items"`PagesCount int   `json:"pagesCount"`
}

然后我们的处理函数解析 JSON,对静态切片进行分页,并在响应中返回新的 JSON。

func ProcessRequest(w http.ResponseWriter, r *http.Request) {var req Request// 解码 JSON 请求if err := json.NewDecoder(r.Body).Decode(&req); err != nil {http.Error(w, err.Error(), http.StatusBadRequest)return}// 对一些静态数据应用 offset 和 limitall := make([]int, 1000)start := req.Offsetend := req.Offset + req.Limitres := Response{Results:    all[start:end],PagesCount: len(all) / req.Limit,}// 发送 JSON 响应if err := json.NewEncoder(w).Encode(res); err != nil {http.Error(w, err.Error(), http.StatusInternalServerError)return}w.WriteHeader(http.StatusOK)
}

你可能已经注意到,这个函数处理切片操作不太好,很容易发生崩溃。另外,如果试图除以0,它也会崩溃。如果我们能在开发期间或仅使用单元测试就发现这一点那很好,但有时并不是所有东西都能被我们看到,而且我们的处理程序可能会将输入传递给其他函数等等。

按照上面的 FuzzEqual 示例,让我们为 ProcessRequest 处理程序实现一个模糊测试。首先我们需要为模糊测试器提供示例输入。这是模糊测试器将用来修改并尝试的数据。我们可以制作一些示例 JSON 请求,并使用 f.Add()[]byte 类型。

func FuzzProcessRequest(f *testing.F) {// 为模糊测试器创建示例输入testRequests := []Request{{Limit: -10, Offset: -10},{Limit: 0, Offset: 0},{Limit: 100, Offset: 100},{Limit: 200, Offset: 200},}// 添加到种子语料库for _, r := range testRequests {if data, err := json.Marshal(r); err == nil {f.Add(data)}}// ...
}

之后我们可以使用 httptest 包创建一个测试 HTTP 服务器并向其发出请求。

注意:由于我们的模糊测试器可能生成无效的非 JSON 请求,最好直接跳过它们并使用 t.Skip() 忽略。我们也可以跳过 BadRequest 错误。

func FuzzProcessRequest(f *testing.F) {// ...// 创建测试服务器srv := httptest.NewServer(http.HandlerFunc(ProcessRequest))defer srv.Close()// 带有单个 []byte 参数的模糊测试目标f.Fuzz(func(t *testing.T, data []byte) {var req Requestif err := json.Unmarshal(data, &req); err != nil {// 跳过模糊测试期间可能生成的无效 JSON 请求t.Skip("invalid json")}// 将数据传递给服务器resp, err := http.DefaultClient.Post(srv.URL, "application/json", bytes.NewBuffer(data))if err != nil {t.Fatalf("unable to call server: %v, data: %s", err, string(data))}defer resp.Body.Close()// 跳过 BadRequest 错误if resp.StatusCode == http.StatusBadRequest {t.Skip("invalid json")}// 检查状态码if resp.StatusCode != http.StatusOK {t.Fatalf("non-200 status code %d", resp.StatusCode)}})
}

我们的模糊测试目标有一个类型为 []byte 的单个参数,其中包含完整的 JSON 请求,但你可以更改它以具有多个参数。

现在一切都准备好了,可以运行我们的模糊测试了。在对 HTTP 服务器进行模糊测试时,你可能需要调整并行工作器的数量,否则负载可能会使测试服务器不堪重负。你可以通过设置 -parallel=1 标志来做到这一点。

go test --fuzz=Fuzz -fuzztime=10s -parallel=1
go test --fuzz=Fuzz -fuzztime=30s
--- FAIL: FuzzProcessRequest (0.02s)--- FAIL: FuzzProcessRequest (0.00s)runtime error: integer divide by zeroruntime error: slice bounds out of range

正如预期的那样,我们会看到上述错误被发现。

我们还可以在 testdata 文件夹中看到模糊测试输入,看看是哪个 JSON 导致了这个失败。这是文件的示例内容:

go test fuzz v1
[]byte("{"limit":0,"offset":0}")

要修复这个问题,我们可以引入输入验证和默认设置:

if req.Limit <= 0 {req.Limit = 1
}
if req.Offset < 0 {req.Offset = 0
}
if req.Offset > len(all) {start = len(all) - 1
}
if end > len(all) {end = len(all)
}

有了这个改变,模糊测试将运行10秒并在没有错误的情况下退出。

结论

为你的 HTTP 服务或任何其他方法编写模糊测试是发现难以发现的 bug 的好方法。模糊测试器可以检测到只在某些奇怪的意外输入时才会发生的难以发现的 bug

看到模糊测试成为 Go 内置测试库的一部分是很棒的,这使得它很容易与常规测试结合使用。注意:在 Go 1.18 之前,开发人员使用 go-fuzz,这也是一个很好的模糊测试工具。


http://www.ppmy.cn/server/143083.html

相关文章

【PGCCC】Postgresql 存储设计

架构图 用户查询指定 page 的数据 首先查询该 page 是否在缓存中&#xff0c;通过 hash table 快速查找它在缓存池的位置 如果存在&#xff0c;那么从缓存池读取返回 如果不存在需要从磁盘读取数据&#xff0c;并且放入到缓存池中&#xff0c;然后返回 postgresql 存储单位 …

力扣周赛:第424场周赛

&#x1f468;‍&#x1f393;作者简介&#xff1a;爱好技术和算法的研究生 &#x1f30c;上期文章&#xff1a;力扣周赛&#xff1a;第422场周赛 &#x1f4da;订阅专栏&#xff1a;力扣周赛 希望文章对你们有所帮助 第一道题模拟题&#xff0c;第二道题经典拆分数组/线段树都…

<项目代码>YOLOv8 番茄识别<目标检测>

YOLOv8是一种单阶段&#xff08;one-stage&#xff09;检测算法&#xff0c;它将目标检测问题转化为一个回归问题&#xff0c;能够在一次前向传播过程中同时完成目标的分类和定位任务。相较于两阶段检测算法&#xff08;如Faster R-CNN&#xff09;&#xff0c;YOLOv8具有更高的…

3D绘制动态爱心Matlab

代码1&#xff1a;动态爱心 function particleHeart1% 调整背景及比例axgca;hold onax.DataAspectRatio[1,1,1];ax.XLim[-25,25];ax.YLim[-25,20];ax.Color[0,0,0];ax.XColornone;ax.YColornone;set(gcf,Color,[0,0,0]);% 散点位置计算函数及扩散函数tFunc(n) rand([1,n]).*pi…

【计算机网络】TCP网络特点2

断开连接 四次挥手 原因 TCP 四次挥手是为了满足 TCP 连接的全双工特性:两个方向都可以自由传输 保证数据传输的完整性&#xff1a;两方都完成了数据发送和接收并且都同意断开连接 可靠地终止连接以及避免数据混淆和错误等需求:每个方向都需要单独确认导致四次挥手过程 这些…

matlab 读取csv

需要跳过第一行表头等信息 1、读取整个文件 csvread(FILENAME)%文件路径 文件名2、指定起始位置 csvread(FILENAME, R, C)%从文件的第R行和第C列开始读取数据 逗号分开3、指定数据范围 csvread(FILENAME, R, C, [R1 C1 R2 C2])%读取从(R1, C1)到(R2, C2)范围内的数据注意&am…

SpringBoot - Async异步处理

目录 一、定义 1、同步调用 2、异步调用 二、示例 1、同步调用 执行类&#xff1a; 测试用例&#xff1a; 运行结果&#xff1a; 2、异步调用 &#xff08;1&#xff09;普通调用 执行类&#xff1a; 测试用例&#xff1a; 运行结果&#xff1a; &#xff08;2&…

4.2 Android NDK 基础概念

1 JavaVM和JNIEnv JNI 定义了两个关键数据结构&#xff0c;JavaVM和JNIEnv。这两者本质上都是指向函数表指针的指针。&#xff08;在 C 版本中&#xff0c;它们是具有指向函数表的指针的类&#xff0c;以及指向该表的每个 JNI 函数的成员函数。&#xff09;JavaVM提供了“调用接…