标签 单元测试 下的文章

Go 1.18新特性前瞻:原生支持Fuzzing测试

本文永久链接 – https://tonybai.com/2021/12/01/first-class-fuzzing-in-go-1-18

今年6月初,Go官博发表了一篇名为《Fuzzing is Beta Ready》的文章,文中称gotip版本已经原生支持Fuzzing并开始了公测。这意味着Fuzzing将以一等公民(first-class citizen)的身份正式加入到即将于2022年2月发布的Go 1.18版本中:


图:关于add fuzz test的issue已经关闭并放入Go1.18里程碑

有了对Fuzzing技术的原生支持后,我相信会有更多代码经过Fuzzing测试,未来不久Go社区的Go代码的安全水平将会得到整体提升。本文我们就来简单聊聊Fuzzing这个Go 1.18版本的新特性。

注意:在Go 1.18版本Beta版发布前要想体验Fuzzing特性,需要自行安装gotip版本,安装方法如下:

$go install golang.org/dl/gotip@latest // go 1.17版本及以后使用go install。go 1.16及之前的版本用go get
$gotip download
$gotip version // 验证安装是否ok

为什么Go要支持Fuzzing

无论是在过去的单机时代,还是在今天的云计算时代,亦或是已经出现苗头的人工智能时代,安全都是程序员在构建软件过程中必不可少且日益重要的考量因素。

同时,安全也是Go语言设计者们在语言设计伊始就为Go设定的一个重要目标。在语言层面,Go提供了很多“安全保障”特性,比如:

  • 显式类型转换,不允许隐式转换;
  • 针对数组、切片的下标越界访问的检查;
  • 不安全代码隔离到unsafe包中,并提供安全使用unsafe包的几条rules;
  • go module构建模式内置包hash校验,放置恶意包对程序的攻击;
  • 雇佣安全专家,提供高质量且及时更新的crypto包,尽量防止使用第三方加解密包带来的不安全性;
  • 支持纯静态编译,避免动态连接到恶意动态库;
  • 原生提供方便测试的工具链,并支持测试覆盖率统计。
  • … …

进入云原生时代后,Go语言成为了云原生基础设施与云原生服务的头部语言,由Go语言建造的基础设施、中间件以及应用服务支撑着这个世界的很多重要系统的运行,这些系统对安全性的要求不言而喻。尤其是在“输入”方面,这些系统都会被要求:无论用户使用什么本文数据、二进制数据,无论用户如何构造协议包、无论用户提供的文件包含何种内容,系统都应该是安全健壮的,不会因为用户的故意攻击而出现处理异常、被操控,甚至崩溃。

这就需要我们的系统面对任何输入的情况下都能正确处理,但传统的代码review、静态分析、人工测试和自动化的单元测试无法穷尽所有输入组合,尤其是难于模拟一些无效的、意料之外的、随机的、边缘的输入数据。

于是,Go社区的一些安全方面的专家就尝试将业界在解决这方面问题的优秀实践引入Go,Fuzzing技术就是其中最重要的一个

什么是Fuzzing?

说了这么半天,啥是Fuzzing

Fuzzing,又叫fuzz testing,中文叫做模糊测试或随机测试。其本质上是一种自动化测试技术,更具体一点,它是一种基于随机输入的自动化测试技术,常被用于发现处理用户输入的代码中存在的bug和问题。

在具体实现上,Fuzzing不需要像单元测试那样使用预先定义好的数据集作为程序输入,而是会通过数据构造引擎自行构造或基于开发人员提供的初始数据构造一些随机数据,并作为输入提供给我们的程序,然后监测程序是否出现panic、断言失败、无限循环等。这些构造出来的随机数据被称为语料(corpus)。另外Fuzz testing不是一次性执行的测试,如果不限制执行次数和执行时间,Fuzz testing会一直执行下去,因此它也是一种持续测试的技术。

Fuzzing技术形态最初的出现可追溯到以打孔卡承载的数据作为计算机输入的时代。那个时候开发人员经常从废纸篓里随便拿出几张打孔卡输入到自己的程序中来验证程序是否可以正确处理这些“随机”的输入。但Fuzzing测试作为一门技术理论走进广大开发者视野是始于1988年的Barton Miller教授的研究成果,

当时任教于威斯康星大学的Barton Miller教授在高级操作系统研究生班(CS736)上的一个秋季项目中运用了随机测试技术,他通过fuzzing对一个UNIX工具进行测试,为该工具自动生成随机输入和命令行参数。该项目旨在通过快速连续执行大量的随机输入来测试UNIX命令行程序的可靠性,直到它们崩溃。当时Miller团队能够使他们所测试的实用程序中的25%至33%崩溃。然后,他们对每个崩溃的程序进行调试,以确定原因,并对每个检测到的故障进行分类。1990年,Miller团队对外发表了这一成果。自那以后,Fuzzing技术便确定了自己独立的研究分支,并逐渐发展演化到今天。在过去的十多年中,Fuzzing测试的技术水平有了很大的提高,以AFL(american fuzzy lop)AFL++libFuzzergo-fuzzsyzkaller等开源项目为代表的Fuzzing项目已经被各种编程语言的开发人员应用于实践,并且取得了不错的“战绩”

Fuzzing是对其他形式的测试、代码审查和静态分析的补充,它通过生成一个有趣的输入语料库,而这些输入几乎不可能用手去想去写出来,因此极易被传统类型的测试所遗漏。Fuzzing可以帮助开发人员发现难以发现的稳定性、逻辑性甚至是安全性方面的错误,特别是当被测系统变得更加复杂时。

更为关键的,也是Fuzzing技术能够流行开来的原因是构建一个Fuzzing测试足够简单。有了上述Fuzzing相关开源工具和开源库的支持,Fuzzing test不再是需要专业知识才能成功使用的技术,并且现代Fuzzing引擎可以更有策略的、更快的、更有效地找到有用的输入语料。

Go对Fuzzing技术支持的简要回顾

说过将Fuzzing技术引入Go,我们不能不提到一个人,它就是Go goroutine调度器的作者、前Intel Black Belt级工程师,现Google工程师的Dmitry Vyukov,它也是Go语言首个fuzzing工具go-fuzz的作者。

2015年的GopherCon大会上,Dmitry Vyukov在其名为“[Go Dynamic Tools]”的presentation中就介绍了go-fuzz。我个人的gocmpp项目就是使用go-fuzz搭建的Fuzz test

之后,Dmitry Vyukov发现虽然通过第三方的fuzzing工具可以解决Go开发者关于Fuzzing的部分需求,但有很多功能特性是通过第三方工具无法实现的。2016年,Dmitry Vyukov在Go官方issue列表中创建“cmd/compile: coverage instrumentation for fuzzing”的issue归纳说明了这些问题,也是从那时开始,Dmitry Vyukov极力推进Fuzzing进入Go原生工具链的。

2017年,Dmitry Vyukov首次提出希望在Go工具链中原生支持Fuzzing的Proposal,并给出了为什么我们需要在Go工具链的支持下使Fuzzing成为Go开发的标准做法的原因

之后,Dmitry还开源了一个名为syzkaller的项目,该项目用Go语言实现了一个针对操作系统内核的Fuzzing引擎。

Dmitry虽然是goroutine调度器的开发者,也是Google员工,但他却不是Go语言团队的一员,也许正是因为这样,又或是Go核心团队对新特性保持的一贯的谨慎态度,该Dmitry的提议一直处理讨论与反馈的状态,直到2020年中旬,Go安全团队的Katie Hockman才正式发布了在Go中原生支持Fuzzing的新提案

新提案与Dmitry的提案的目标是相同的,都是期望在Go工具链中加入对Fuzzing的原生支持。不同的是,新提案中的API稍微复杂一些,新提案允许以编程方式设置初始语料库的种子语料,并支持字节字符串以外的输入类型,包括原生的基本数据类型(整型、浮点、字符串等)、复合数据类型(数组、map、结构体等)以及实现了BinaryMarshaler/BinaryUnmarshaler或TextMarshaler/TextUnmarshaler接口的自定义类型。

不过Go fuzzing没有如预期加入到Go 1.17版本中,再继续打磨了半年后,Go 1.18版本正式接受了原生支持Fuzzing这个特性,Fuzzing成为Go的“一等公民”。

如何使用成为Go“一等公民”的Fuzzing

作为普通Go开发人员,我们无需关心Fuzzing引擎是如何实现的,我们只需根据Go提供的Fuzzing API使用就好了。前面也提到过了,Fuzzing技术之所以可以流行到现在,更多也是因为其使用起来十分简单。

Go 1.18将fuzz testing纳入了go test工具链,与单元测试、性能基准测试等一起成为了Go原生测试工具链中的重要成员。

go fuzzing test的测试用例与普通的测试用例(TestXxx)、性能基准测试(BenchmarkXxx)等一样放在xx_test.go中,只不过用例对应的函数名样式换为了FuzzXxx了。一个简单的Fuzzing test用例如下:

func FuzzXxx(f *testing.F) {
    // 设置种子语料(可选)

    // 执行Fuzzing
    f.Fuzz(func(t *testing.T, b []byte) {
        //... ...
    })
}

我们通过go test -fuzz命令即可执行所有xx_test.go中以Fuzz为前缀的Fuzz testing用例。不过这里要注意的是go test -fuzz会先进行普通TestXxx的用例执行,之后才会执行FuzzXxx。

我们看到:每个FuzzXxx函数的参数列表是固定的,fuzzing在testing包中新增了一个F结构体类型,其与testing.T、testing.B是类似的,用于表示一次Fuzzing test的执行,结构体变量中存储了此次fuzzing test执行的上下文信息。

FuzzXxx函数的最主要只能就是调用testing.F的Fuzz方法对目标函数/方法执行测试。testing.F的Fuzz方法接受一个函数类型的参数,按照fuzzing提案的说明,该函数的第一个参数必须是*testing.T,后面可以是多个输入参数,且输入参数的类型不局限于传统的byte切片,还可以是上面提到过的原生的基本数据类型(整型、浮点、字符串等)、复合数据类型(数组、map、结构体等)以及实现了BinaryMarshaler/BinaryUnmarshaler或TextMarshaler/TextUnmarshaler接口的自定义类型。

Fuzz方法会根据目标所需的输入参数类型生成fuzzing后的数据,并将这些数据作为输入参数传给其参数代表的函数。

Go开发人员也可以通过testing.F的Add方法为fuzzing过程提供初始的种子语料(seed corpus),后续fuzzing test会基于这些种子语料进行fuzzing,虽然这不是必须的,但提供与目标处理逻辑相关的种子语料,可能会加速被测目标中问题的暴露。

当然我们也可以不通过testing.F的Add方法来提供种子语料,go fuzzing还允许你通过文件的形式为每个FuzzXxx函数提供种子语料,以FuzzXxx为例,我们可以在testdata/fuzz/FuzzXxx目录下以文件的形式放置其初始语料,比如:

// testdata/fuzz/FuzzXxx/corpus1
go test fuzz v1
[]byte("ABC\xa8\x8c\xb3G\xfc")

在这个种子语料文件中,第一行go test fuzz v1是go fuzzing要求的文件头,用于标识这是一个种子语料文件,并且使用的编解码器的版本为v1。而下面的一行则是种子语料,其实这行就是一个go代码的片段,fuzzing工具会将其解码后直接作为输入赋值给被测目标。后续的fuzzing过程也是基于这个种子语料的。注意:种子语料可以有很多个,不同语料单独放置在一行中即可。目前,这个种子语料文件需要手工编辑,后续go团队可能会提供相关工具以方便对种子语料文件的编辑,尤其是针对一些二进制数据的种子语料。

下面我们用一个例子来看看如何使用go原生fuzzing。我这里还以我的gocmpp项目为例,对项目中的Submit消息的Unpack方法进行fuzz testing。

我们先来看不提供初始种子语料的情况。

不提供初始种子语料

虽说FuzzXxx测试用例可以与普通用例TestXxx放在一个xxx_test.go文件中,但例子中为了着重强调我们是在写Fuzz testing用例,我们将FuzzXxxx函数放在submit_fuzz_test.go中,下面是不显式提供初始种子语料的FuzzSubmit:

// submit_fuzz_test.go

//go:build go1.18
// +build go1.18

package cmpp_test

import (
    "testing"

    cmpp "github.com/bigwhite/gocmpp"
)

func FuzzSubmit(f *testing.F) {
    f.Fuzz(func(t *testing.T, data []byte) {
        p := &cmpp.Cmpp2SubmitReqPkt{}
        _ = p.Unpack(data)
    })
}

我们看到,为了兼容go 1.18之前的版本,我们使用build指示符来告诉编译器这个文件只在go 1.18及后续版本才参与编译,这样go 1.17及之前的版本会ignore这个源文件。

使用下面命令我们来运行一下这个fuzz test:

$gotip test -v -fuzz  .
=== RUN   TestTypeString
--- PASS: TestTypeString (0.00s)
... ...
=== RUN   TestCmppTerminateRspPktPack
--- PASS: TestCmppTerminateRspPktPack (0.00s)
=== RUN   TestCmppTerminateRspUnpack
--- PASS: TestCmppTerminateRspUnpack (0.00s)
=== FUZZ  FuzzSubmit
fuzz: elapsed: 0s, gathering baseline coverage: 0/38 completed
fuzz: elapsed: 0s, gathering baseline coverage: 38/38 completed, now fuzzing with 8 workers
fuzz: elapsed: 3s, execs: 403378 (134408/sec), new interesting: 0 (total: 38)
fuzz: elapsed: 6s, execs: 800451 (132348/sec), new interesting: 0 (total: 38)
fuzz: elapsed: 9s, execs: 1104435 (101366/sec), new interesting: 0 (total: 38)
fuzz: elapsed: 12s, execs: 1346809 (80789/sec), new interesting: 0 (total: 38)
fuzz: elapsed: 15s, execs: 1718297 (123747/sec), new interesting: 1 (total: 39)
^Cfuzz: elapsed: 16s, execs: 1771361 (92170/sec), new interesting: 1 (total: 39)
--- PASS: FuzzSubmit (15.59s)
PASS
ok      github.com/bigwhite/gocmpp  15.607s

通过上面的执行我们得到几点信息;

  • gotip test -fuzz会先进行普通TestXxx的用例执行,之后才会执行FuzzXxx。

  • fuzz testing默认会一直执行下去,直到遇到crash。如果要限制fuzz testing的执行时间,可以使用-fuzztime,比如下面的命令允许fuzz testing只执行10s:

$gotip test -v  -fuzztime 10s -fuzz .
  • 覆盖率引导的fuzzing

我们看到:测试输出内容中包含“gathering baseline coverage字样”,这里要告诉大家的是,go fuzzing使用的是一种名为“覆盖率引导的fuzzing(coverage-guided fuzzing)”。我们知道Fuzzing测试使用的是随机生成的随机数据对被测目标进行测试,如果仅仅是用凭空随机的方式生成的输入很难有效找到边缘情况,当今大多数现代的fuzzing工具都是用了coverage-guided fuzzing的引擎来驱动测试,它可以确定新生成的语料是否能覆盖到新的代码路径。Dmitry在“Fuzzing support for Go”一文中曾经对coverage-guided fuzzing的引擎的工作逻辑做过如下描述:

从一些(可能是空的)输入的语料库开始
for {
    从语料库中随机选择一个输入
    对输入进行变异(mutate)
    执行变异后的输入并收集代码覆盖率
    如果该输入给出了新的覆盖率,则将其添加到语料库中
}

收集代码覆盖率数据和检测一个输入”给出新的覆盖率”并非易事,这也是为什么很多第三方fuzzing工具难于实现的原因,而加入到go原生工具链中,这些特性可借助Go编译器以及已有的设施来完成。

  • 缓存新加入的语料

在上面go test -fuzz执行过程中,新生成的可以覆盖新代码路径的语料放置在哪里了呢?Go fuzzing会将其缓存在cache路径下,我在本机的gocache路径下发现如下与fuzz有关的内容:

//GOCACHE="/Users/tonybai/Library/Caches/go-build"

$ls /Users/tonybai/Library/Caches/go-build/fuzz/github.com/bigwhite/gocmpp/FuzzSubmit
01e40385855b3d317d42bf7ba4a9118702d867492fe2cd52bc84e554769480e5
06ba4bdb19de593e669c642987e270fe2488d4d58ecd712db136a3e011071253
086eabebec22d361087ce571571d86b1cbe1ad3e713f981473275f2c8fa64f09
... ...

go fuzzing会为每个FuzzXxx函数建立对应的语料缓存。目前fuzz cache的默认路径为$GOCACHE/fuzz/包路径/FuzzXxx,后续可能会提供环境变量或cmd option,以允许开发人员自定义fuzzing语料的缓存路径。

前面我们说过,fuzzing test默认会一直执行下去,如果不限制执行次数和执行时间,执行Fuzzing test的机器要有足够的存储才行。而要想清理过多的fuzz缓存,用gotip clean -cache是不行的,需要为clean加上-fuzzcache才可以清除fuzz的cache。

使用Add方法提供种子语料

下面是通过testing.F的Add方法为fuzzing test提供种子语料的例子:

//go:build go1.18
// +build go1.18

package cmpp_test

import (
    "testing"

    cmpp "github.com/bigwhite/gocmpp"
)

func FuzzSubmit(f *testing.F) {
    data := []byte{
        0x00, 0x00, 0x00, 0xbd, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x74, 0x65, 0x73, 0x74, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x02, 0x31, 0x33, 0x35, 0x30, 0x30, 0x30, 0x30, 0x32, 0x36, 0x39, 0x36, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x39, 0x30, 0x30, 0x30, 0x30,
        0x31, 0x30, 0x32, 0x31, 0x30, 0x00, 0x00, 0x00, 0x00, 0x31, 0x35, 0x31, 0x31, 0x30, 0x35, 0x31,
        0x33, 0x31, 0x35, 0x35, 0x35, 0x31, 0x30, 0x31, 0x2b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x39, 0x30, 0x30, 0x30, 0x30,
        0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x01, 0x31, 0x33, 0x35, 0x30, 0x30, 0x30, 0x30, 0x32, 0x36, 0x39, 0x36, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1e, 0x6d, 0x4b, 0x8b, 0xd5, 0x00, 0x67, 0x00, 0x6f, 0x00,
        0x63, 0x00, 0x6d, 0x00, 0x70, 0x00, 0x70, 0x00, 0x20, 0x00, 0x73, 0x00, 0x75, 0x00, 0x62, 0x00,
        0x6d, 0x00, 0x69, 0x00, 0x74, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    }
    f.Add(data[8:])
    f.Fuzz(func(t *testing.T, data []byte) {
        p := &cmpp.Cmpp2SubmitReqPkt{}
        _ = p.Unpack(data)
    })
}

在上面代码中,我们使用了一个正确的cmpp2 submit消息包的数据作为种子语料,后续fuzzing会基于该语料做mutate,生成其他语料对Submit的Unpack方法进行测试。执行该fuzz test的输出与不设置种子语料的输出差别不大:

$gotip test -fuzz .
fuzz: elapsed: 0s, gathering baseline coverage: 0/1 completed
fuzz: elapsed: 0s, gathering baseline coverage: 1/1 completed, now fuzzing with 8 workers
fuzz: elapsed: 3s, execs: 395490 (131760/sec), new interesting: 20 (total: 20)
fuzz: elapsed: 6s, execs: 795164 (133251/sec), new interesting: 22 (total: 22)
fuzz: elapsed: 9s, execs: 1106761 (103875/sec), new interesting: 22 (total: 22)
^Cfuzz: elapsed: 11s, execs: 1281577 (92654/sec), new interesting: 22 (total: 22)
PASS
ok      github.com/bigwhite/gocmpp  10.917s

使用testdata/fuzz/FuzzXxx目录提供种子语料

除了通过Add方法添加种子语料外,我们还可以在项目的testdata/fuzz/FuzzXxx目录下为FuzzXxx用例提供种子语料:

$tree testdata
testdata
└── fuzz
    └── FuzzSubmit
        └── corpus1

$cat testdata/fuzz/FuzzSubmit/corpus1
go test fuzz v1
[]byte("ABC\xa8\x8c\xb3G\xfc")

我们把FuzzSubmit代码恢复为不带有Add的形式:

func FuzzSubmit(f *testing.F) {
    f.Fuzz(func(t *testing.T, data []byte) {
        p := &cmpp.Cmpp2SubmitReqPkt{}
        _ = p.Unpack(data)
    })
}

执行这一用例,输出入下结果:

$gotip test -fuzz .
fuzz: elapsed: 0s, gathering baseline coverage: 0/23 completed
fuzz: elapsed: 0s, gathering baseline coverage: 23/23 completed, now fuzzing with 8 workers
fuzz: elapsed: 3s, execs: 355689 (118485/sec), new interesting: 3 (total: 25)
fuzz: elapsed: 6s, execs: 623934 (89390/sec), new interesting: 5 (total: 27)
fuzz: elapsed: 9s, execs: 756400 (44154/sec), new interesting: 5 (total: 27)
^Cfuzz: elapsed: 10s, execs: 771910 (28387/sec), new interesting: 5 (total: 27)
PASS
ok      github.com/bigwhite/gocmpp  9.564s

模拟crash

Fuzzing技术作为其他测试的一种补充,可以用于发现代码中潜在的不易被发现的问题,但这一过程有时候也是很漫长的。因此在这里我们模拟一个crash的产生来看看:当fuzzing test过程中发现代码bug时,fuzzing都做了那些事情。

我们在被测方法Unpack中故意放置了一个panic语句:

// submit.go
func (p *Cmpp2SubmitReqPkt) Unpack(data []byte) error {
    ... ...
    panic("panic for fuzz demo")
    ... ...
}

这样,当fuzzing test执行到这里时Unpack方法就会panic,而fuzzing会生成crash报告并终止fuzz testing执行。

但要注意的是:go test -fuzz是先运行TestXxx,然后再运行FuzzXxx的,如果在TestXxx中发生了panic,那么go fuzzing是不会生成crash报告的。另外还有一点要格外注意:go fuzzing将种子语料作为输入时出现的panic也是不会生成crash报告的,因为,此时对数据的fuzzing还没有真正开始。因此,这里我们使用上面那个不带有种子语料的FuzzSubmit。

好了,我们执行一下fuzzing test:

$gotip test -fuzz .
fuzz: elapsed: 0s, gathering baseline coverage: 0/27 completed
fuzz: minimizing 148-byte crash file
fuzz: elapsed: 0s, gathering baseline coverage: 0/27 completed
--- FAIL: FuzzSubmit (0.03s)
    --- FAIL: FuzzSubmit (0.00s)
        testing.go:1319: panic: panic for fuzz demo
            goroutine 49 [running]:
            runtime/debug.Stack()
                /Users/tonybai/sdk/gotip/src/runtime/debug/stack.go:24 +0x90
            testing.tRunner.func1()
                /Users/tonybai/sdk/gotip/src/testing/testing.go:1319 +0x1f2
            panic({0x11ca860, 0x1241148})
                /Users/tonybai/sdk/gotip/src/runtime/panic.go:838 +0x207
            github.com/bigwhite/gocmpp.(*Cmpp2SubmitReqPkt).Unpack(0xc00025a5a0, {0xc000024400, 0x1, 0x80})
                /Users/tonybai/Go/src/github.com/bigwhite/gocmpp/submit.go:262 +0x625
            github.com/bigwhite/gocmpp_test.FuzzSubmit.func1(0x0?, {0xc000024400, 0x1, 0x80})
                /Users/tonybai/Go/src/github.com/bigwhite/gocmpp/submit_fuzz_test.go:32 +0x45
            reflect.Value.call({0x11ccf40?, 0x12111c8?, 0x13?}, {0x1200df5, 0x4}, {0xc000217080, 0x2, 0x2?})
                /Users/tonybai/sdk/gotip/src/reflect/value.go:543 +0x814
            reflect.Value.Call({0x11ccf40?, 0x12111c8?, 0x10f9980?}, {0xc000217080, 0x2, 0x2})
                /Users/tonybai/sdk/gotip/src/reflect/value.go:339 +0xbf
            testing.(*F).Fuzz.func1.1(0x0?)
                /Users/tonybai/sdk/gotip/src/testing/fuzz.go:327 +0x20b
            testing.tRunner(0xc000201860, 0xc0003a0900)
                /Users/tonybai/sdk/gotip/src/testing/testing.go:1409 +0x102
            created by testing.(*F).Fuzz.func1
                /Users/tonybai/sdk/gotip/src/testing/fuzz.go:316 +0x5b8

    Crash written to testdata/fuzz/FuzzSubmit/582528ddfad69eb57775199a43e0f9fd5c94bba343ce7bb6724d4ebafe311ed4
    To re-run:
    go test github.com/bigwhite/gocmpp -run=FuzzSubmit/582528ddfad69eb57775199a43e0f9fd5c94bba343ce7bb6724d4ebafe311ed4
FAIL
exit status 1
FAIL    github.com/bigwhite/gocmpp  0.037s

我们看到go fuzzing在执行第一个测试时就发生了crash,fuzzing将crash输出了crash报告,并将引发这一crash的语料放入了testdata/fuzz/FuzzSubmit目录下:

$tree testdata
testdata
└── fuzz
    └── FuzzSubmit
        └── 582528ddfad69eb57775199a43e0f9fd5c94bba343ce7bb6724d4ebafe311ed4

查看一下引发crash的语料:

$cat testdata/fuzz/FuzzSubmit/582528ddfad69eb57775199a43e0f9fd5c94bba343ce7bb6724d4ebafe311ed4
go test fuzz v1
[]byte("0")

如果是真实的fuzz测试引发了crash,我们可以将该语料提取出来,建立针对它的TestXxx或为现有TestXxx添加一条测试数据来验证目标方法是否真实存在缺陷,如果的确存在缺陷,我们就需要修复它,并在修复后再次运行TestXxx,以保证我们的修复是有效的。

小结

在即将到来的Go 1.18中,Go fuzzing将正式成为Go的“一等公民”,Go将原生支持Fuzzing。不过Go fuzzing刚刚加入,可能还存在各种问题,根据以往经验,在2-3个版本后,go fuzzing必将成熟稳定起来,到那时,Gopher们就又拥有了一柄挖掘潜在bug和安全问题的利器了。

参考资料


“Gopher部落”知识星球正式转正(从试运营星球变成了正式星球)!“gopher部落”旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!部落目前虽小,但持续力很强,欢迎大家加入!

img{512x368}

img{512x368}
img{512x368}
img{512x368}

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。2020年4月8日,中国三大电信运营商联合发布《5G消息白皮书》,51短信平台也会全新升级到“51商用消息平台”,全面支持5G RCS消息。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博:https://weibo.com/bigwhite20xx
  • 微信公众号:iamtonybai
  • 博客:tonybai.com
  • github: https://github.com/bigwhite
  • “Gopher部落”知识星球:https://public.zsxq.com/groups/51284458844544

微信赞赏:
img{512x368}

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

Go语言项目的安全评估技术

在今年夏天我们对Kubernetes的评估成功之后,我们收到了大量Go项目的安全评估需求。为此,我们将在其他编译语言中使用过的安全评估技术和策略调整适配到多个Go项目中。

我们从了解语言的设计开始,识别出开发人员可能无法完全理解语言语义特性的地方。多数这些被误解的语义来自我们向客户报告的调查结果以及对语言本身的独立研究。尽管不是详尽无遗,但其中一些问题领域包括作用域、协程、错误处理和依赖管理。值得注意的是,其中许多与运行时没有直接关系。默认情况下,Go运行时本身的设计是安全的,避免了很多类似C语言的漏洞。

对根本原因有了更好地理解后,我们搜索了现有的能帮助我们快速有效检测客户端代码库的工具。结果我们找到一些静态和动态开源工具,其中包括了一些与Go无关的工具。为了配合这些工具使用,我们还确定了几种有助于检测的编译器配置。

一. 静态分析

由于Go是一种编译型语言,因此编译器在生成二进制可执行文件之前就检测并杜绝了许多潜在的错误模式。虽然对于新的Go开发人员来说,这些编译器的输出比较烦,但是这些警告对于防止意外行为以及保持代码的清洁和可读性非常重要。

静态分析趋向于捕获很多未包括在编译器错误和警告中的悬而未决的问题。在Go语言生态系统中,有许多不同的工具,例如go-vetstaticcheckanalysis包中的工具。这些工具通常会识别出诸如变量遮蔽、不安全的指针使用以及未使用的函数返回值之类的问题。调查这些工具显示警告的项目区域通常会发现可被利用(进行安全攻击)的功能特性。

这些工具绝不是完美的。例如,go-vet可能会错过非常常见的问题,例如下面例子中这种。

package main

import "fmt"

func A() (bool, error) { return false, fmt.Errorf("I get overridden!") }

func B() (bool, error) { return true, nil }

func main() {
    aSuccess, err := A()
    bSuccess, err := B()
    if err != nil {
        fmt.Println(err)
    }
    fmt.Println(aSuccess, ":", bSuccess)
}

这个例子未使用A函数的err返回值,并在表达式左侧为bSuccess赋值期间立即重新对err做了赋值。编译器针对这种情况不会提供警告,而go-vet也不会检测到该问题;errcheck也不会。实际上,能成功识别这种情况的工具是前面提到的staticcheck和ineffassign,它们将A的错误返回值标识为未使用或无效。

示例程序的输出以及errcheck,go-vet,staticcheck和ineffassign的检查结果如下:

$ go run .
false : true
$ errcheck .
$ go vet .
$ staticcheck .
main.go:5:50: error strings should not be capitalized (ST1005)
main.go:5:50: error strings should not end with punctuation or a newline (ST1005)
main.go:10:12: this value of err is never used (SA4006)
$ ineffassign .
main.go:10:12: ineffectual assignment to err

当您深入研究此示例时,您可能会想知道为什么编译器没有针对此问题发出警告。当程序中有未使用的变量时,Go编译器将出错,但此示例成功通过编译。这是由“短变量声明”的语义引起的。下面是短变量声明的语法规范:

ShortVarDecl = IdentifierList ":=" ExpressionList .

根据规范,短变量声明具有重新声明变量的特殊功能,只要:

  • 重新声明在多变量短声明中。
  • 重新声明的变量在同一代码块或函数的参数列表中声明较早。
  • 重新声明的变量与先前的声明具有相同的类型。
  • 声明中至少有一个非空白变量(“_”)是新变量。

所有这些约束在上一个示例中均得到满足,从而防止了编译器针对此问题产生编译错误。

许多工具都具有类似这样的极端情况,即它们在识别相关问题或识别问题但以不同的方式描述时均未成功。使问题复杂化的是,这些工具通常需要先构建Go源代码,然后才能执行分析。如果分析人员无法轻松构建代码库或其依赖项,这将使第三方安全评估变得复杂。

尽管存在这些困难,但只要付出一点点努力,这些工具就可以很好地提示我们在项目中从何处查找问题。我们建议至少使用gosecgo-vetstaticcheck。对大多数代码库而言,这些工具具有良好的文档和人机工效。他们还提供了针对常见问题的多种检查(例如ineffassign或errcheck)。但是,要对特定类型的问题进行更深入的分析,可能必须使用更具体的分析器,直接针对SSA开发定制的工具或使用semmle

二. 动态分析

一旦执行了静态分析并检查了结果,动态分析技术通常是获得更深层结果的下一步。由于Go的内存安全性,动态分析通常发现的问题是导致硬崩溃(hard crash)或程序状态无效的问题。Go社区已经建立了各种工具和方法来帮助识别Go生态系统中这些类型的问题。此外,可以改造现有的与语言无关的工具以满足Go动态分析的需求,我们将在下面展示。

1. 模糊测试

Go语言领域中最著名的动态测试工具可能是Dimitry Vyukovgo-fuzz了。该工具使您可以快速有效地实施模糊测试,并且它已经有了不错的战利品。更高级的用户在猎错过程中可能还会发现分布式的模糊测试libFuzzer的支持非常有用。

Google还发布了一个更原生的模糊器(fuzzer),它拥有一个与上面的go-fuzz相似的名字:gofuzz。它通过初始化具有随机值的结构来帮助用户。与Dimitry的go-fuzz不同,Google的gofuzz不会生成夹具(harness)或协助提供存储崩溃时的输出信息、模糊输入或任何其他类型的信息。尽管这对于测试某些目标可能是不利的,但它使轻量级且可扩展的框架成为可能。

为了简洁起见,我们请您参考各自自述文件中这两个工具的示例。

2. 属性测试(property test)

译注:属性测试指编写对你的代码来说为真的逻辑语句(即“属性”),然后使用自动化工具来生成测试输入(一般来说,是指某种特定类型的随机生成输入数据),并观察程序接受该输入时属性是否保持不变。如果某个输入违反了某一条属性,则证明用户程序存在错误 – 摘自网络。

与传统的模糊测试方法不同,Go的testing包(通常用于单元测试和集成测试)为Go函数的“黑盒测试” 提供了testing/quick子包。换句话说,它提供了属性测试的基本原语。给定一个函数和生成器,该包可用于构建夹具,以测试在给定输入生成器范围的情况下潜在的属性违规。以下示例是直接摘自官方文档。

func TestOddMultipleOfThree(t *testing.T) {
    f := func(x int) bool {
        y := OddMultipleOfThree(x)
        return y%2 == 1 && y%3 == 0
    }
    if err := quick.Check(f, nil); err != nil {
        t.Error(err)
    }
}

上面示例正在测试OddMultipleOfThree函数,其返回值应始终为3的奇数倍。如果不是,则f函数将返回false并将违反该属性。这是由quick.Check功能检测到的。

虽然此包提供的功能对于属性测试的简单应用是可以接受的,但重要的属性通常不能很好地适合这种基本界面。为了解决这些缺点,诞生了leanovate/gopter框架。Gopter为常见的Go类型提供了各种各样的生成器,并且支持您创建与Gopter兼容的自定义生成器。通过gopter/commands子包还支持状态测试,这对于测试跨操作序列的属性是否有用很有有帮助。除此之外,当违反属性时,Gopter会缩小生成的输入。请参阅下面的输出中输入收缩的属性测试的简要示例。

Compute结构的测试夹具:

package main_test
import (
  "github.com/leanovate/gopter"
  "github.com/leanovate/gopter/gen"
  "github.com/leanovate/gopter/prop"
  "math"
  "testing"
)

type Compute struct {
  A uint32
  B uint32
}

func (c *Compute) CoerceInt () { c.A = c.A % 10; c.B = c.B % 10; }
func (c Compute) Add () uint32 { return c.A + c.B }
func (c Compute) Subtract () uint32 { return c.A - c.B }
func (c Compute) Divide () uint32 { return c.A / c.B }
func (c Compute) Multiply () uint32 { return c.A * c.B }

func TestCompute(t *testing.T) {
  parameters := gopter.DefaultTestParameters()
  parameters.Rng.Seed(1234) // Just for this example to generate reproducible results

  properties := gopter.NewProperties(parameters)

  properties.Property("Add should never fail.", prop.ForAll(
    func(a uint32, b uint32) bool {
      inpCompute := Compute{A: a, B: b}
      inpCompute.CoerceInt()
      inpCompute.Add()
      return true
    },
    gen.UInt32Range(0, math.MaxUint32),
    gen.UInt32Range(0, math.MaxUint32),
  ))

  properties.Property("Subtract should never fail.", prop.ForAll(
    func(a uint32, b uint32) bool {
      inpCompute := Compute{A: a, B: b}
      inpCompute.CoerceInt()
      inpCompute.Subtract()
      return true
    },
    gen.UInt32Range(0, math.MaxUint32),
    gen.UInt32Range(0, math.MaxUint32),
  ))

  properties.Property("Multiply should never fail.", prop.ForAll(
    func(a uint32, b uint32) bool {
      inpCompute := Compute{A: a, B: b}
      inpCompute.CoerceInt()
      inpCompute.Multiply()
      return true
    },
    gen.UInt32Range(0, math.MaxUint32),
    gen.UInt32Range(0, math.MaxUint32),
  ))

  properties.Property("Divide should never fail.", prop.ForAll(
    func(a uint32, b uint32) bool {
      inpCompute := Compute{A: a, B: b}
      inpCompute.CoerceInt()
      inpCompute.Divide()
      return true
    },
    gen.UInt32Range(0, math.MaxUint32),
    gen.UInt32Range(0, math.MaxUint32),
  ))

  properties.TestingRun(t)
}

执行测试夹具并观察属性测试的输出(除法失败):

user@host:~/Desktop/gopter_math$ go test
+ Add should never fail.: OK, passed 100 tests.
Elapsed time: 253.291µs
+ Subtract should never fail.: OK, passed 100 tests.
Elapsed time: 203.55µs
+ Multiply should never fail.: OK, passed 100 tests.
Elapsed time: 203.464µs
! Divide should never fail.: Error on property evaluation after 1 passed
   tests: Check paniced: runtime error: integer divide by zero
goroutine 5 [running]:
runtime/debug.Stack(0x5583a0, 0xc0000ccd80, 0xc00009d580)
    /usr/lib/go-1.12/src/runtime/debug/stack.go:24 +0x9d
github.com/leanovate/gopter/prop.checkConditionFunc.func2.1(0xc00009d9c0)
    /home/user/go/src/github.com/leanovate/gopter/prop/check_condition_func.g
  o:43 +0xeb
panic(0x554480, 0x6aa440)
    /usr/lib/go-1.12/src/runtime/panic.go:522 +0x1b5
_/home/user/Desktop/gopter_math_test.Compute.Divide(...)
    /home/user/Desktop/gopter_math/main_test.go:18
_/home/user/Desktop/gopter_math_test.TestCompute.func4(0x0, 0x0)
    /home/user/Desktop/gopter_math/main_test.go:63 +0x3d
# snip for brevity;

ARG_0: 0
ARG_0_ORIGINAL (1 shrinks): 117380812
ARG_1: 0
ARG_1_ORIGINAL (1 shrinks): 3287875120
Elapsed time: 183.113µs
--- FAIL: TestCompute (0.00s)
    properties.go:57: failed with initial seed: 1568637945819043624
FAIL
exit status 1
FAIL    _/home/user/Desktop/gopter_math 0.004s

3. 故障注入

在攻击Go系统时,故障注入令人惊讶地有效。我们使用此方法发现的最常见错误包括对error类型的处理。因为error在Go中只是一种类型,所以当它返回时,它不会像panic语句那样自行改变程序的执行流程。我们通过强制生成来自最低级别(内核)的错误来识别此类错误。由于Go会生成静态二进制文件,因此必须在不使用LD_PRELOAD的情况下注入故障。我们的工具之一KRF使我们能够做到这一点。

在我们最近的Kubernetes代码库评估中,我们使用KRF找到了一个vendored依赖深处的问题,只需通过随机为进程和其子进程发起的read和write系统调用制造故障。该技术对通常与底层系统交互的Kubelet十分有效。该错误是在ionice命令出现错误时触发的,未向STDOUT输出信息并向STDERR发送错误。记录错误后,将继续执行而不是将STDERR的错误返回给调用方。这导致STDOUT后续被索引,从而导致索引超出范围导致运行时panic。

下面是导致kubelet panic的调用栈信息:

E0320 19:31:54.493854    6450 fs.go:591] Failed to read from stdout for cmd [ionice -c3 nice -n 19 du -s /var/lib/docker/overlay2/bbfc9596c0b12fb31c70db5ffdb78f47af303247bea7b93eee2cbf9062e307d8/diff] - read |0: bad file descriptor
panic: runtime error: index out of range

goroutine 289 [running]:
k8s.io/kubernetes/vendor/github.com/google/cadvisor/fs.GetDirDiskUsage(0xc001192c60, 0x5e, 0x1bf08eb000, 0x1, 0x0, 0xc0011a7188)
    /workspace/anago-v1.13.4-beta.0.55+c27b913fddd1a6/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/vendor/github.com/google/cadvisor/fs/fs.go:600 +0xa86
k8s.io/kubernetes/vendor/github.com/google/cadvisor/fs.(*RealFsInfo).GetDirDiskUsage(0xc000bdbb60, 0xc001192c60, 0x5e, 0x1bf08eb000, 0x0, 0x0, 0x0)
    /workspace/anago-v1.13.4-beta.0.55+c27b913fddd1a6/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/vendor/github.com/google/cadvisor/fs/fs.go:565 +0x89
k8s.io/kubernetes/vendor/github.com/google/cadvisor/container/common.(*realFsHandler).update(0xc000ee7560, 0x0, 0x0)
    /workspace/anago-v1.13.4-beta.0.55+c27b913fddd1a6/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/vendor/github.com/google/cadvisor/container/common/fsHandler.go:82 +0x36a
k8s.io/kubernetes/vendor/github.com/google/cadvisor/container/common.(*realFsHandler).trackUsage(0xc000ee7560)
    /workspace/anago-v1.13.4-beta.0.55+c27b913fddd1a6/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/vendor/github.com/google/cadvisor/container/common/fsHandler.go:120 +0x13b
created by
k8s.io/kubernetes/vendor/github.com/google/cadvisor/container/common.(*realFsHandler).Start
    /workspace/anago-v1.13.4-beta.0.55+c27b913fddd1a6/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/vendor/github.com/google/cadvisor/container/common/fsHandler.go:142 +0x3f

下面例子:记录了STDERR日志但未将error返回调用方。

stdoutb, souterr := ioutil.ReadAll(stdoutp)
if souterr != nil {
    klog.Errorf("Failed to read from stdout for cmd %v - %v", cmd.Args, souterr)
}

当stdout为空,也尝试使用索引,这是运行时出现panic的原因:

usageInKb, err := strconv.ParseUint(strings.Fields(stdout)[0], 10, 64)

更完整的包含重现上述问题的步骤,可参见我们的Kubernetes最终报告附录G(第109页),那里详细介绍了针对Kubelet使用KRF的方法。

Go的编译器还允许将测量工具包含在二进制文件中,从而可以在运行时检测race状况,这对于将潜在的race识别为攻击者非常有用,但也可以用来识别对defer、panic和recover的不正确处理。我们构建了Trailofbits/on-edge来做到这一点:识别函数入口点和函数panic点之间的全局状态变化,并通过Go race检测器”泄露”此信息。有关OnEdge的更多详细信息,请参见我们以前的博客文章“在Go中选择正确panic的方式”

实践中,我们建议使用:

  • dvyukov/go-fuzz为组件解析输入建立夹具
  • google/gofuzz用于测试结构验证
  • leanovate/gopter用于增强现有的单元和集成测试以及测试规范的正确性
  • Trailofbits/krf和Trailofbits/on-edge用于测试错误处理。

除KRF外,所有这些工具在实践中都需要付出一些努力。

三. 利用编译器的优势

Go编译器具有许多内置功能和指令(directive),可帮助我们查找错误。这些功能隐藏在各种开关中中,并且需要一些配置才能达到我们的目的。

1. 颠覆类型系统

有时在尝试测试系统功能时,导出函数不是我们要测试的。要获得对所需的函数的测试访问权,可能需要重命名许多函数,以便可以将其导出,这可能会很麻烦。要解决此问题,可以使用编译器的build指令(directive)进行名称链接(name linking)以及导出系统的访问控制。作为此功能的示例,下面的程序(从Stack Overflow答案中提取)访问未导出的reflect.typelinks函数,并随后迭代类型链接表以识别已编译程序中存在的类型。

下面是使用linkname build directive的Stack Overflow答案的通用版本:

package main

import (
    "fmt"
    "reflect"
    "unsafe"
)

func Typelinks() (sections []unsafe.Pointer, offset [][]int32) {
    return typelinks()
}

//go:linkname typelinks reflect.typelinks
func typelinks() (sections []unsafe.Pointer, offset [][]int32)

func Add(p unsafe.Pointer, x uintptr, whySafe string) unsafe.Pointer {
    return add(p, x, whySafe)
}

//go:linkname add reflect.add
func add(p unsafe.Pointer, x uintptr, whySafe string) unsafe.Pointer

func main() {
    sections, offsets := Typelinks()
    for i, base := range sections {
        for _, offset := range offsets[i] {
            typeAddr := Add(base, uintptr(offset), "")
            typ := reflect.TypeOf(*(*interface{})(unsafe.Pointer(&typeAddr)))
            fmt.Println(typ)
        }
    }
}

下面是typelinks表的输出:

$ go run main.go
**reflect.rtype
**runtime._defer
**runtime._type
**runtime.funcval
**runtime.g
**runtime.hchan
**runtime.heapArena
**runtime.itab
**runtime.mcache
**runtime.moduledata
**runtime.mspan
**runtime.notInHeap
**runtime.p
**runtime.special
**runtime.sudog
**runtime.treapNode
**sync.entry
**sync.poolChainElt
**syscall.Dirent
**uint8

如果需要在运行时进行更精细的控制(即,不仅仅是linkname指令),则可以编写Go的中间汇编码,并在编译过程中包括它。尽管在某些地方它可能不完整且有些过时,但是teh-cmc/go-internals提供了有关Go如何组装函数的很好的介绍。

2. 编译器生成的覆盖图

为了帮助进行测试,Go编译器可以执行预处理以生成coverage信息。这旨在标识单元测试和集成测试的测试覆盖范围信息,但是我们也可以使用它来标识由模糊测试和属性测试生成的测试覆盖范围。Filippo Valsorda在博客文章中提供了一个简单的示例。

3. 类型宽度安全

Go支持根据目标平台自动确定整数和浮点数的大小。但是,它也允许使用固定宽度的定义,例如int32和int64。当混合使用自动宽度和固定宽度大小时,对于跨多个目标平台的行为,可能会出现错误的假设。

针对目标的32位和64位平台构建进行测试将有助于识别特定于平台的问题。这些问题通常在执行验证、解码或类型转换的时候发现,原因在于对源和目标类型属性做出了不正确的假设。在Kubernetes安全评估中就有一些这样的示例,特别是TOB-K8S-015:使用strconv.Atoi并将结果向下转换时的溢出(Kubernetes最终报告中的第42页),下面是这个示例。

// updatePodContainers updates PodSpec.Containers.Ports with passed parameters.
func updatePodPorts(params map[string]string, podSpec *v1.PodSpec) (err error) {
    port := -1
    hostPort := -1
    if len(params["port"]) > 0 {
        port, err = strconv.Atoi(params["port"]) // <-- this should parse port as strconv.ParseUint(params["port"], 10, 16)
        if err != nil {
            return err
        }
    }
       // (...)
    // Don't include the port if it was not specified.
    if len(params["port"]) > 0 {
        podSpec.Containers[0].Ports = []v1.ContainerPort{
            {
                ContainerPort: int32(port), // <-- this should later just be uint16(port)
            },
        }

错误的类型宽度假设导致的溢出:

root@k8s-1:/home/vagrant# kubectl expose deployment nginx-deployment --port 4294967377 --target-port 4294967376
E0402 09:25:31.888983    3625 intstr.go:61] value: 4294967376 overflows int32
goroutine 1 [running]:
runtime/debug.Stack(0xc000e54eb8, 0xc4f1e9b8, 0xa3ce32e2a3d43b34)
    /usr/local/go/src/runtime/debug/stack.go:24 +0xa7
k8s.io/kubernetes/vendor/k8s.io/apimachinery/pkg/util/intstr.FromInt(0x100000050, 0xa, 0x100000050, 0x0, 0x0)
...
service/nginx-deployment exposed

实际上,很少需要颠覆类型系统。最需要的测试目标已经是导出了的,可以通过import获得。我们建议仅在需要助手和测试类似的未导出函数时才使用此功能。至于测试类型宽度安全性,我们建议您尽可能对所有目标进行编译,即使没有直接支持也是如此,因为不同目标上的问题可能更明显。最后,我们建议至少生成包含单元测试和集成测试的项目的覆盖率报告。它有助于确定未经直接测试的区域,这些区域可以优先进行审查。

四. 有关依赖的说明

在诸如JavaScript和Rust的语言中,依赖项管理器内置了对依赖项审核的支持-扫描项目依赖项以查找已知存在漏洞的版本。在Go中,不存在这样的工具,至少没有处于公开可用且非实验状态的。

这种缺乏可能是由于存在多种不同的依赖关系管理方法:go-modgo-getvendored等。这些不同的方法使用根本不同的实现方案,导致无法直接识别依赖关系及其版本。此外,在某些情况下,开发人员通常会随后修改其vendor的依赖的源代码。

在Go的开发过程中,依赖管理问题的解决已经取得了进展,大多数开发人员都在朝使用go mod的方向发展。这样就可以通过项目中的go.mod跟踪和依赖项并进行版本控制,从而为以后的依赖项扫描工作打开了大门。我们可以在OWASP DependencyCheck工具中看到此类工作的示例,该工具是具有实验性质的go mod插件。

五. 结论

最终,Go生态系统中有许多可以使用的工具。尽管大多数情况是完全不同的,但是各种静态分析工具可帮助识别给定项目中的“悬而未决的问题”。当寻求更深层次的关注时,可以使用模糊测试,属性测试和故障注入工具。编译器配置随后增强了动态技术,使构建测试夹具和评估其有效性变得更加容易。

本文翻译自“Security assessment techniques for Go projects”


我的网课“Kubernetes实战:高可用集群搭建、配置、运维与应用”在慕课网上线了,感谢小伙伴们学习支持!

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/
smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

微博:https://weibo.com/bigwhite20xx
微信公众号:iamtonybai
博客:tonybai.com
github: https://github.com/bigwhite

微信赞赏:
img{512x368}

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats