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

news/2024/11/13 21:30:27/
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/news/1546754.html

相关文章

基本和引用数据类型以及对象字面量(day14)

一、基本和引用数据类型 1. 基本数据类型 String Number Boolean Null Undefined 引用数据类型 Object 2. JS中的变量都是保存到栈内存中 基本数据类型的值直接在栈内存中存储&#xff0c; 值与值之间时独立存在&#xff0c;修改一个变量不会影响其他变量 3. 对象&#xff…

应用于新能源汽车NCV4275CDT50RKG车规级LDO线性电压调节器芯片

关于车规级芯片&#xff08;Automotive Grade Chip&#xff09;&#xff0c;车规级芯片是专门用于汽车行业的芯片&#xff0c;具有高可靠性、高稳定性和低功耗等特点&#xff0c;以满足汽车电子系统的严格要求。这些芯片通常用于车载电子控制单元&#xff08;ECU&#xff09;和…

FluentUI使用

首先向Qt Qml FluentUI组件库的作者zhuzichu520致敬&#xff01; 一、源码下载地址&#xff1a; 1&#xff09;GitHub - zhuzichu520/FluentUI: FluentUI for QML 2&#xff09;GitCode - 全球开发者的开源社区,开源代码托管平台 二、Qt6下载地址&#xff1a; qt-online-i…

Pinpoint(APM)进阶--Pinot指标采集(System Metric/Inspector)

接上文 Pinpoint使用Pinot进行指标数据存储&#xff0c;Pinot流摄入需要Kafka 本文详解Kafka和Pinot的安装部署&#xff0c;以及Pinpoint的指标采集 Pinot 简介 Apache Pinot是一个实时分布式OLAP数据存储&#xff0c;专为低延迟、高吞吐量分析而构建&#xff0c;非常适合面…

哪些人群适合考取 PostgreSQL 数据库 PGCM 证书?

#postgresql#&#xff0c;作为开源数据库领域的佼佼者&#xff0c;凭借其强大的功能和广泛的应用场景&#xff0c;吸引了大量数据库从业者的关注。它代表着持有者在PostgreSQL数据库管理、优化、安全和高可用性设计等方面的专家级技能。 PGCM证书适合那些具备扎实理论基础和一…

某m大厂面经1

mybatisplus有什么优点 mybatisplus如何进行多表查询 项目中有哪些表 xxl-job如何实现分布式定时任务 feign和nacos之间怎么交互 springboot服务启动流程 怎么用jar包启动类 maven打包的形式 dependence和dependencemanagement区别 redis应用场景 redisson怎么实现 …

普通用户切换到 root 用户不需要输入密码配置(Ubuntu20)

在 Ubuntu 系统中&#xff0c;允许一个普通用户切换到 root 用户而不需要输入密码&#xff0c;可以通过以下步骤配置 sudo 设置来实现。 步骤&#xff1a; 打开 sudoers 文件进行编辑&#xff1a; 在终端中&#xff0c;输入以下命令来编辑 sudoers 文件&#xff1a; sudo visu…

如何评估Elasticsearch查询性能的具体指标?

Elasticsearch提供了多种内置监控工具&#xff0c;以下是一些主要的监控工具&#xff1a; Kibana&#xff1a; Kibana是一个强大的可视化工具&#xff0c;它内置了多种图表和仪表板&#xff0c;可以直观地显示Elasticsearch集群的状态。 Elasticsearch Monitoring API&#xf…