原文链接:静态分析工具的评估测试-云社区-华为云
忙忙碌碌又一年,似乎始终都在工具的误报、漏报、能力对比三个问题之间打转转。说“反思”也好、“回溯”也好、“复盘”也好,也和同事、朋友、同行讨论过多次测试用例对这个三个问题解决的作用,也发生过激烈的争论。基本上分为三个门派:
这三个门派,应该各有众多弟子,也都会存在于各种不同的场景中。
早就想把一些思考总结出来,转眼离跨年就没几天,匆匆忙忙将手上的信息做个汇总,先挖个坑吧。
还是先说故事。那是 2014 年参加的一个测试驱动(TDD)的培训,培训是 TDD 推广的志愿者组织的,在一个咖啡馆里搞的,周末两天的免费培训。
培训过程中的一张图和一个视频让我至今记忆尤新。
上面的两个图(原来培训的那个图找不到了,自己随手涂鸦了一下)。
左边是经过完整的系统的测试的软件产品,每个节点都通过测试,这样一层层的搭建起来的系统。看着就坚实可靠。
右边的测试则是随意的,很多地方都缺失了。任何一个风吹草动,一个异常都可能造成整个大厦倾覆。
不用说大家立刻就可以看懂,那个软件产品更可靠,更让人放心。
视频是电影《垂直极限》中开头的一段。虽然过去 20 多年了,但这部电影仍然被奉做山难电影的经典之作,有兴趣的朋友还是可以找来看一看的。
一个风和日丽的日子里,老爸带着儿子、女儿在一个高耸、陡峭的山峰上攀岩。他们已经来到了山峰的一半,在他们的上面个还有另一群攀岩爱好者。
老爸一边整理着自己的安全栓(攀岩过程中打在岩石中,然后利用登山绳固定自己的安全扣),一边对儿子说:“检查下你妹妹的安全栓。”
儿子看向下面的妹妹,妹妹说:“告诉他我们已经不是小孩子了。”
儿子打趣的对老爸说:“爸,她还需要一个安全栓。”
爸爸看向下面的兄妹两人说:“万一出事,那样可撑不住。安妮,我不管你有多老练,聪明的攀岩者都会做好安全措施,两个栓才安全,三个更好。在岩壁上再放个安全栓,我们才继续攀。”
女儿生气的对爸爸说:“爸,你别听哥哥的,他在开玩笑,我放了三个安全栓。”
电影总是这样,一段舒缓的节奏后面,总会有段紧张的让人喘不过气来的场面。不出意外,意外就会发生。
话音未落,先是一个登山包从山上面坠落下来,快速的经过三人。上面的菜鸟在登山的过程中背包意外脱落,绑着的登山包的登山绳造成一连串的连锁反应,两个登山者也不幸地被连带着坠落下去。老爸、儿子也被这一连串的变故拖累到挂在了半空中,女儿的三个安全栓无法承担三个人的重量,一个、两个先后崩飞,只剩最后的一个安全栓勉强的维持着女儿,以及挂在半空中的老爸和儿子。最后在老爸的要求下,儿子不得不割断了绳索,女儿和儿子看着老爸坠了下去。
看到这里,大家都不禁须臾不已,再看看上面的图,对系统的完整的测试有了更深刻的认识。
在软件的开发过程中,每一个节点的测试都是在为系统增加一个安全栓。由这样一层层搭建起来的系统,整个系统才是安全、可靠的。否则任何一个意外都可能将整个系统带飞。做工具和做单点能力验证不同,需要更多的测试节点来保障工具的稳定性和可靠性。这个从学院里出来的大部分朋友还没有意识到这个问题,加之项目管理的问题,就会把各种坑坑洼洼带到生产中,使后期维护陷入无尽的深渊。
大家都喜欢做 0 到 1 的事情,出彩啊。很少有人愿意做为 0 到 1 填坑的基础工作,但一个工具要生存,能够赢得最终的胜利,需要无数的这些幕后英雄。就像长津湖战役一样,指挥很重要,但更多的是需要后面无数的无名英雄,那些人才是撑起整个胜利的英雄。
那么对于静态分析工具,我们该如何构建一个稳定、可靠的静态分析工具?如何评价一个静态分析工具的检查能力?
自从程序的诞生,程序分析便紧随其后,人们试图通过一个程序来分析编制的程序,保证编制程序运行结果的正确性。尽管后面的莱斯定理给出了这个问题的“不可判定性”,但并不会妨碍程序分析在这方面的卓越表现。
尽管现在大模型能够帮助我们生成程序代码和对已有程序进行问题检查,这似乎可以绕过我们一值以来使用的模式匹配的检查方式,可以简化模式的提炼,和根据模式再编写检查规则。 但如何保障生成代码的安全性和可靠性,以及如何评估大模型的能力,仍然将是人工智能在今后很长一段时间需要解决的问题。
本文试图通过下面对三组业界测试用例的分析,希望能够给静态分析工具的测试和评估一个指导性的启发。
2005 年美国国家标准与技术研究院(National Institute of Standards and Technology (NIST)),简称NIST,下属的软件质量组,成立了软件保障指标和工具评估项目(Software Assurance Metrics And Tool Evaluation (SAMATE)),简称SAMATE项目。项目组的主要目的是通过开发支持软件工具评估的方法、衡量工具和技术的有效性以及识别工具和方法中的差距来改善软件保障。主要工作包括定义错误类,收集具有已知错误的程序语料库,以及更好地了解工具的有效性。
围绕这个目标,软件保障指标和工具评估项目(SAMATE)建立了两个子项目:
软件保障参考数据集(Software Assurance Reference Dataset (SARD)),简称SARD,用于收集整理C、C++、Java、PHP 和 C#针对软件弱点的测试用例集;
软件工具博览会(Static Analysis Tool Exposition (SATE)),简称SATE,用于工具制造商对用例进行测试和工具研讨。截至 2021 年,已经举办了六场 SATE 活动。
Juliet 测试用例集就是软件保障参考数据集(SARD)下的一个是用于检测 C/C++ 和 Java 程序已知缺陷的集合。
2010 年 12 月 1.0版本
Juliet 测试用例集最早的 1.0 发布于2010 年 12 月,由软件保障指标和工具评估项目(SAMATE)的开发团队完成,名字选取了当时软件保障参考数据集(SARD)的第十个贡献者:国际无线电(International Radiotelephony)的字母表中的第十个单词 “Juliet” 而的得名。
2011 年 1.1 - 1.1.1 版本
Juliet 1.1 版本 是 Juliet 的开发团队根据多个因素为选定的缺陷创建了测试用例,包括团队的经验、缺陷的重要性或严重性以及其出现的频率。测试用例涵盖了 2011 年 CWE/SAN TOP 25 个最危险的编程错误中的 14 个。剩下的 11 个缺陷是设计问题,例如 CWE-862 授权机制缺失 和 CWE-250 带着不必要的权限执行,这些问题不适合用静态分析的方式来检测,所以未包含在测试用例中。
2012 年 1.2 版本
2012 年的 Juliet 1.2 版本。基本形成了现在使用版本。是我们现在使用最多的版本,程序分析、自动修复、深度学习的论文基本上都使用了这里面的用例,用于证明理论、工具的有效性。
2017 年 1.3 版本
只对1.2 版本的个别错误进行了修正。
2022 年 1.3.1 版本
增加了SARIF 的支持,试图通过SARIF 精确的给出告警的位置信息,但SARIF中的数据对数据流的支持的并不好,不能完全依靠现有的SARIF版本。
注:关于 SARIF请参考:
测试用例使用CWE作为命名和组织的基础。测试用例力求对目标缺陷使用最具体的CWE条目。每个测试用例文件与一个CWE条目相关联。
组成部分 | 描述 | 是否强制 |
---|---|---|
CWE | 字符 | 是 |
CWE ID | CWE的编号 | 是 |
_ | 分隔,下划线 | 是 |
CWE短描述 | CWE英文名称的缩短版本,单词之间用下划线"_"连接 | 是 |
__ | 分隔,两个下划线"_" | 是 |
问题场景 | 表示用例场景。场景描述单词之间用下划线"_"连接 | 是 |
_ | 分隔,下划线,问题场景和案例编号之间进行连接 | 是 |
案例编号 | 基础案例编号采用“01” | 是 |
一个案例多文件定义 | 对同一个案例,如使用多个文件,可采用a,b等进行区别,或采用下划线加不同的作用定义,例如_good或_bad,表示正例或反例 | 否 |
例如:
单文件用例:
多文件用例, 下面的一组文件构成了一个用例:
^bad$
;^good$
;^good(\d+|G2B\d*|B2G\d*)$
;
^badSource$
;^badSink$
;^good(G2B\d*|B2G\d*)?Source$
;^good(G2B\d*|B2G\d*)?Sink$
。Juliet 还为每个问题(CWE)枚举了一些发生场景,一些场景还使用模板结合控制流和数据流进行了枚举,从而更加广泛的测试问题发生的可能性,以提高问题检测的覆盖率。
Juliet 一共采用了三类模板:
Juliet Java
Juliet C/C++
模板类型 | C | CPP | 用例数 |
---|---|---|---|
point-flaw | 259 | 18 | 277 |
sources-sink | 499 | 233 | 723 |
sources-sinks | 357 | 152 | 509 |
Total | 1106 | 403 | 1509 |
Juliet 用例还针对每个场景结合控制流和数据流进行了枚举,以达到每个场景在不同代码条件下的检测能力的覆盖。
从下表我们看到类型基本上涵盖了编程语言通常所需要的基本语法,例如条件判断(if),条件分支(swith),循环(while,for),函数间和程序间不同层数的调用,以及不同类型的参数传递方式。
案列编号 | 案例类型(流类型) | 类型描述 | C | CPP | JAVA |
---|---|---|---|---|---|
01 | 无 | 基线——最简单的缺陷形式 | Y | Y | Y |
02 | 控制流 | if(true) 和 if(false) | Y | Y | Y |
03 | 控制流 | if(5==5) 和 if(5!=5) | Y | Y | Y |
04 | 控制流 | if(PRIVATE_STATIC_FINAL_TRUE) 和 if(PRIVATE_STATIC_FINAL_FALSE) | Y | Y | Y |
05 | 控制流 | if(privateTrue) 和 if(privateFalse) | Y | Y | Y |
06 | 控制流 | if(PRIVATE_STATIC_FINAL_FIVE==5) 和 if(PRIVATE_STATIC_FINAL_FIVE!=5) | Y | Y | Y |
07 | 控制流 | if(privateFive==5) 和 if(privateFive!=5) | Y | Y | Y |
08 | 控制流 | if(privateReturnsTrue()) 和 if(privateReturnsFalse()) | Y | Y | Y |
09 | 控制流 | if(IO.STATIC_FINAL_TRUE) 和 if(IO.STATIC_FINAL_FALSE) | Y | Y | Y |
10 | 控制流 | if(IO.staticTrue) 和 if(IO.staticFalse) | Y | Y | Y |
11 | 控制流 | if(IO.staticReturnsTrue()) 和 if(IO.staticReturnsFalse()) | Y | Y | Y |
12 | 控制流 | if(IO.staticReturnsTrueOrFalse()) | Y | Y | Y |
13 | 控制流 | if(IO.STATIC_FINAL_FIVE==5) 和 if(IO.STATIC_FINAL_FIVE!=5) | Y | Y | Y |
14 | 控制流 | if(IO.staticFive==5) 和 if(IO.staticFive!=5) | Y | Y | Y |
15 | 控制流 | switch(6) and switch(7) | Y | Y | Y |
16 | 控制流 | while(true) | Y | Y | Y |
17 | 控制流 | for 循环 | Y | Y | Y |
18 | 控制流 | goto 语句 | Y | Y | N |
21 | 控制流 | 流由私有变量的值控制。所有方法都包含在一个文件中。 | Y | Y | Y |
22 | 控制流 | 流由公共静态变量的值控制。source和sink在两个不同的文件里。 | Y | Y | Y |
31 | 数据流 | 在同一方法中使用数据副本流动 | Y | Y | Y |
32 | 数据流 | 在同一个函数中使用两个指向同一个值的指针的数据流 | Y | Y | N |
33 | 数据流 | 在同一函数中使用对数据的 C++ 引用 | N | Y | N |
34 | 数据流 | 使用包含两个访问相同数据的方法的联合(在同一函数内) | Y | Y | N |
41 | 数据流 | 作为参数从一个方法传递到同一类中的另一个方法 | Y | Y | Y |
42 | 数据流 | 在同一类中从一个方法返回到另一个方法 | Y | Y | Y |
43 | 数据流 | 数据使用 C++ 引用从同一源文件中的一个函数流向另一个函数 | N | Y | N |
44 | 数据流 | 数据作为参数从一个函数传递到通过函数指针调用的同一源文件中的函数 | Y | Y | N |
45 | 数据流 | 作为私有类成员变量从一个方法传递到同一类中的另一个方法 | Y | Y | Y |
51 | 数据流 | 在同一个包的不同类中作为参数从一个方法传递到另一个方法 | Y | Y | Y |
52 | 数据流 | 在同一个包的三个不同类中作为参数从一个方法传递到另一个方法 | Y | Y | Y |
53 | 数据流 | 在同一个包的四个不同类中作为参数从一个方法传递到另一个方法 | Y | Y | Y |
54 | 数据流 | 在同一个包的五个不同类中作为参数从一个方法传递到另一个方法 | Y | Y | Y |
61 | 数据流 | 在同一个包的不同类中通过一个方法的返回值传递到另一个方法 | Y | Y | Y |
62 | 数据流 | 使用 C++ 引用从不同源文件中的一个函数到另一个函数的数据流 | N | Y | N |
63 | 数据流 | 指向不同源文件中从一个函数传递到另一个函数的数据的指针 | Y | Y | N |
64 | 数据流 | void 指向不同源文件中从一个函数传递到另一个函数的数据的指针 | Y | Y | N |
65 | 数据流 | 数据作为参数从一个函数传递到通过函数指针调用的不同源文件中的函数 | Y | Y | N |
66 | 数据流 | 在同一个包中的不同类中通过数组从一个方法传递到另一个方法 | Y | Y | Y |
67 | 数据流 | 在同一个包中的不同类中通过类从一个方法传递到另一个方法 | Y | Y | Y |
68 | 数据流 | 在同一个包中的不同类中通过类的成员变量从一个方法传递到另一个方法 | Y | Y | Y |
71 | 数据流 | 在同一个包中的不同类中通过类的引用从一个方法传递到另一个方法 | N | N | Y |
72 | 数据流 | 在同一个包中的不同类中将向量从一个方法传递到另一个方法 | N | Y | Y |
73 | 数据流 | 在同一个包中的不同类中将LinkedList 从一个方法传递到另一个方法 | N | Y | Y |
74 | 数据流 | 在同一个包中的不同类中将HashMap从一个方法传递到另一个方法 | N | Y | Y |
75 | 数据流 | 将序列化对象从一个方法传递到同一包中不同类中的另一个方法 | N | N | Y |
81 | 数据流 | 将参数传递给通过引用调用的抽象方法 | N | Y | Y |
82 | 数据流 | 将参数传递给通过指针调用的虚函数的数据 | N | Y | N |
83 | 数据流 | 通过在堆栈上声明类对象将数据传递给类构造函数和析构函数 | N | Y | N |
84 | 数据流 | 通过在堆上声明类对象并在使用后将其删除来传递给类构造函数和析构函数的数据 | N | Y | N |
Juliet 可以说是堪称完美的测试用例集,但随着时间的推移,编程语言的迭代,它也显现出一些缺点:
OWASP(Open Worldwide Application Security Project (OWASP)) 基金会致力于通过其社区主导的开源软件项目、全球数百个分会、数万名成员以及举办当地和全球会议来提高软件的安全性。
OWASP Benchmark Project 是一个 Java 测试套件,旨在评估自动化软件漏洞检测工具的准确性、覆盖率和速度。
下面以 1.2 版本为例进行说明。
从下面这个表可以看出 Benchmark 更多的注重覆盖 Web 类的问题的检查,同时重点覆盖了 OWASP TOP 10 中能够通过静态检查工具检查的问题。关于 OWASP TOP 10 可以参考:CWE 4.6 和 OWASP TOP10(2021)。
Benchmark 用例集主要以加密问题,以及注入类问题为主,这也巧合的与静态检查技术相互匹配。加密问题多用抽象语法树(AST)的遍历来返现加密函数,并对其做出判断。而注入类问题多用数据流的污点分析技术来追踪外部输入是否会对爆发点形成可达的路径。有关污点分析技术,可以参考:使用污点分析检查log4j问题。
检测问题 | CWE TOP 25(2023) | OWASP TOP 10(2021) | 正确用例 | 错误用例 | 用例总数 |
---|---|---|---|---|---|
CWE-22 对路径名的限制不恰当(路径遍历) | 8 | A01:2021-中断访问控制 | 135 | 133 | 268 |
CWE-327 使用已被攻破或存在风险的密码学算法 | - | A02:2021-加密故障 | 116 | 130 | 246 |
CWE-328 可逆的单向哈希 | - | A02:2021-加密故障 | 107 | 129 | 236 |
CWE-330 使用不充分的随机数 | - | A02:2021-加密故障 | 275 | 218 | 493 |
CWE-501 违背信任边界 | - | A04:2021-不安全的设计 | 43 | 83 | 126 |
CWE-614 HTTPS会话中未设置’Secure’属性的敏感Cookie | - | A02:2021-加密故障 | 31 | 36 | 67 |
CWE-643 XPath表达式中数据转义处理不恰当(XPath注入) | - | A03:2021-注入 | 20 | 15 | 35 |
CWE-78 OS命令中使用的特殊元素转义处理不恰当(OS命令注入) | 5 | A03:2021-注入 | 125 | 126 | 251 |
CWE-79 在Web页面生成时对输入的转义处理不恰当(跨站脚本) | 2 | A03:2021-注入 | 209 | 246 | 455 |
CWE-89 SQL命令中使用的特殊元素转义处理不恰当(SQL注入) | 3 | A03:2021-注入 | 232 | 272 | 504 |
CWE-90 LDAP查询中使用的特殊元素转义处理不恰当(LDAP注入) | - | A03:2021-注入 | 32 | 27 | 59 |
Benchmark 用例对每个问题采用: 场景枚举 + 组合的方式完成用例的设计,并通过此方法形成问题的覆盖。
这里以我们熟悉的:CWE89 SQL注入问题来说明这种用例的设计方式。用例集中CWE 89 SQL注入问题一共有 232 个正例,272 个反例,共计504 个用例。
因为 SQL 是注入是通过外部不可信数据,传播到 SQL 脚本执行的位置而导致的安全问题。这个外部数据传播的过程可以分为:
用例使用了我们常用的从 http 请求中得到外部数据,然后将数据以不同方法存入不同类型的字段。用例中列举了下面 9 种不同的方法。例如放入:字符串、枚举、数组等。
用例接收到数据后,使用不同的传递方式,向程序内传递,并对信息采用不同的操作方式进行加工。用例中列举了下面 10 种不同的方法。例如:通过 创建一个新类然后调用函数传递、if 条件表达式、内部类等。
最终数据会拼装成 SQL 语句,并通过不同的调用方式执行。用例中列举了 3 类,28 种不同的执行方式。
得到上面三种基本节点后,通过组合的方式形成用例。下表列举了 CWE89 SQL注入 272 个反例组合的场景:
OwaspBenchmark 应为涵盖了Web应用的主要安全问题,使用例集基本上成为了Web 应用安全测试的基本用例集。但它也存在一些缺点。
针对xAST领域缺乏有效衡量技术能力标准的业界痛点,蚂蚁安全团队联合蚂蚁程序分析团队、浙江大学网络空间安全学院的20余位专家学者,共同设计了xAST评价体系及其测试样本套件Benchmark,致力于成为应用安全测试工具的“度量衡”。
目标:打造具备行业共识的xAST能力评价体系技术标准
价值:衡量xAST产品技术能力,指引xAST技术发展方向,辅助企业产品选型
用例设计的核心思想是:分层设计,降低评价复杂度。
从底层到上层分成引擎能力、规则能力和产品化能力这三层。对这三层分别设计评价体系和测试样本,既降低了每一层评价的复杂度,又使测试结果可以直接反映问题出在哪一层。
看的出,用例集的设计者试图希望结合Juliet、Owaspbenchmark 的优点,在形成一种分层的评估测试方式。来完善前面两个用例集在语法层面的不足。
目前用例集只推出了一个雏形,还在建设中。但用例的设计思想不能不说给予我们不小的启发。
最后再来总结下,理想的测试用例集应该是怎样的。
写在最后,测试用例集的结果,只能反映一个工具的基础能力,毕竟用例的复杂度有限,并不能完全取代通过实际的工程来打磨检查工具。
一般的程序员只需要在问题和实现上建立一条通道就好,但程序分析的程序员却需要考虑各种程序员实现问题的可能性。