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

ops/2024/11/17 13:03:20/
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/ops/134428.html

相关文章

T265相机双目鱼眼+imu联合标定(全记录)

最近工作用到t265&#xff0c;记录一遍标定过程 1.安装驱动 首先安装realsense驱动&#xff0c;因为笔者之前使用过d435i&#xff0c;装的librealsense版本为2.55.1&#xff0c;直接使用t265会出现找不到设备的问题&#xff0c;经查阅发现是因为realsense在2.53.1后就不再支持…

Android - Pixel 6a 手机OS 由 Android 15 降级到 Android 14 操作记录

Pixel 6a 手机由 Android 14 升级到 Android 15了&#xff0c;但是由于一些原因又想降级回 Android 14&#xff0c; 能降吗&#xff1f;该怎么降级呢&#xff1f;本篇文章来记述实际操作过程&#xff0c;希望能给想做相同操作的人一些帮助。 答案当然是能降&#xff0c;而且我…

什么是 Go 语言?

Go 语言&#xff08;也称为 Golang&#xff09;是由 Google 开发的一种开源编程语言。它最初由 Rob Pike、Ken Thompson 和 Robert Griesemer 等人于 2007 年设计&#xff0c;经过两年的研发&#xff0c;于 2009 年首次公开发布。Go 语言的设计目标是提高编程效率&#xff0c;特…

基于语法树的SQL自动改写工具开发系列(2)-使用PYTHON进行简单SQL改写的开发实战

一、前言 前面一篇写了如何搭建环境&#xff0c;本文接着讲怎么使用antlr4进行开发。 二、实战 根据上一篇&#xff0c;基于语法树的SQL自动改写工具开发系列&#xff08;1&#xff09;-离线安装语法树解析工具antlr4-DA-技术分享-M版,先在本地部署好开发环境。 DEMO 1 写…

-bash: /home/xxx/anaconda3/bin/conda: No such file or directory

Linux系统中移动用户的配置文件后&#xff0c;Anaconda出现-bash: /home/shaocaiyin2023/anaconda3/bin/conda: No such file or directory错误提示。 查看PATH变量信息 echo $PATH 检查环境变量是否包含移动之后的文件目录&#xff0c;主要到*/anaconda3/bin这一层。如果没有…

STM32 设计的较为复杂的物联网项目,包括智能家居控制系统,涵盖了硬件和软件的详细设计。

使用 STM32 设计的较为复杂的物联网项目&#xff0c;包括智能家居控制系统&#xff0c;涵盖了硬件和软件的详细设计。 一、硬件设计 微控制器&#xff1a;选择 STM32F4 系列微控制器&#xff0c;如 STM32F407ZGT6&#xff0c;具有高性能和丰富的外设资源。 传感器模块&#x…

11个c语言编程练习题

0. 钞票和硬币 money.c 读取一个带有两个小数位的浮点数&#xff0c;代表货币价值。将该值分解为多种钞票和硬币的和&#xff0c;要求使用的钞票和硬币的总数量尽可能少。 货币面值有100&#xff0c;50&#xff0c;20&#xff0c;10&#xff0c;5&#xff0c;1&#xff0c;0.…

鸿蒙生态:开发者的新征程与挑战并存

随着科技的飞速发展&#xff0c;我们迎来了一个新的时代——鸿蒙系统的时代。作为开发者&#xff0c;我有幸见证了鸿蒙生态的崛起&#xff0c;并亲身参与其中。今天&#xff0c;我想和大家分享我对鸿蒙生态的认知&#xff0c;以及在这一生态下开发时遇到的挑战和我的应对策略。…