代码审查热点:使用 Semgrep 进行代码安全检查
推荐超级课程:
@TOC
什么是热点?
在这个上下文中,热点是代码中可能包含安全漏洞的部分。您不是“总是”在寻找特定的问题,而是在寻找不良实践、常见错误、不安全的配置,简而言之,就是通常会发生坏事的地方。
在现代社会软件项目中审查每一行代码是不可能的。因此,我们搜索一个心理或书面的关键字列表,如 SSLv3
、MD5
、memcpy
或 encrypt/decrypt
。我们不确定我们会找到什么,但这些都是寻找错误的好地方。
静态分析规则的类型
您(作为安全工程师)应该有两组独立的、以安全为中心的静态分析规则:
security
:针对开发人员。hotspots
:针对安全工程师。
安全规则
security
规则检测特定漏洞(例如 log4j
)。理想情况下,它们应该是轻量级的,并且返回零个误报。我应该能够让开发人员有信心地在他们的工作流程(例如 CI/CD 管道或编辑器)中部署我的规则,并且相信我不会浪费他们的时间。如果不是这样,他们将停止信任我,并丢弃(或绕过)应用程序安全设备。我会做同样的事情。
热点规则
hotspots
是为我准备的。我想找到代码中容易出错的部分。我通常可以快速审查后丢弃误报。这些规则应该非常杂乱,但不要花太多时间减少噪音。
现有文献回顾
我并不是在提出一个新颖的想法。我是从其他人那里学到的。
每个人都有一个热点列表
每个安全工程师都有一个个人(心理或书面的)关键字列表,这些关键字是随着时间的推移积累起来的。irsdl 的 .NET Interesting Keywords to Find Deserialization Issues 是一个好例子。您可以通过快速搜索找到类似的列表。收集这些列表很有趣。
硬编码密钥检测器
硬编码的秘密是热点。有数以亿计的产品和正则表达式用于查找 API 密钥、密码和加密密钥。结果通常有很高的误报率,需要手动审查。
Semgrep 规则中的审计类别
Semgrep 规则存储库将它们称为 audit
规则,并将它们存储在 security/audit
下。您可以使用 p/security-audit
策略运行它们。
如果安全规则正在阻止使用不良模式(例如格式化的 SQL 字符串),我们建议在您的命名空间中追加审计。这将其与专门旨在检测漏洞的安全规则区分开来。
审计不应该在 Semgrep 规则存储库中的安全类别下
Semgrep 抨击即将开始。
在 security
下运行规则也会运行更烦人的 audit
。在我看来,security
和 audit
应该是单独的类别。您可以使用策略来避免这个问题,但本地规则仍然存在问题。
Semgrep 可以使用 --config /path/to/rules/
运行本地规则。它将运行路径中的每个规则以及任何子目录中的规则。因此,--config semgrep-rules/python/lang/security
也会运行 python/lang/security/audit
中的规则。
我们可以直接使用注册表而不下载规则。--config r/python.lang.security
将运行注册表下 /python/lang/security
中的所有规则,包括 audit
。
这种行为并不理想。audit
规则在设计上就是杂乱的。我已经以不同的方式组织了我们的内部 Semgrep 规则。例如,我们有 python.lang.security
和 python.lang.hotspots
。我可以将 security
中的规则传递给开发人员,并将杂乱的 hotspots
保留给我们自己。
Microsoft 应用程序检查器
Microsoft 应用程序检查器
是一个“代码中有什么”的工具。它具有内置功能,如 cryptography
或 authentication
。每个规则都属于一个功能,并包含一个关键字/正则表达式的列表。如果一个规则有命中,最终报告将包括该功能。例如,如果代码中有 md5
,则应用程序具有 cryptography
。
我玩了几个星期的应用程序检查器和 DevSkim
(一个使用相同规则格式的 IDE 检查器),但决定它们不适合我。应用程序检查器旨在展示功能(例如,此应用程序具有 authentication
),但我对导航和审查结果感兴趣。
几天前,我看了看 weggli
中的一些 C++ 规则。weggli
是由 Google Project Zero 的 Felix Wilhelm
开发的 C/C++ 静态分析工具。weggli 和 Semgrep 使用相同的解析器(Tree-Sitter
)并且具有类似的规则模式。自述文件中有一系列示例,我将其中一些移植到了 Semgrep 中。
我还发现了 Julien Voisin 和 Jordy (Oblivion)
的 与 Weggli 一起玩
。他们在 Linux 代码库上运行了一些自定义 weggli 规则。博客给了我关于 Semgrep 规则的想法(请参阅后面讨论的 sizeof(*ptr)
规则)。
不同类型的热点
我为热点创建了一个简单的类别。我将定义每个类别并讨论示例。
- 不安全的配置:具有易受攻击配置的(通常是第三方)组件。
- 危险函数:使用这些函数通常是一个安全问题。
- 危险模式:通常安全的方法和结构的不安全使用。
- 有趣的关键字:变量/类/方法名称和注释中的特定术语。
1. 不安全的配置
框架、库或基础设施的配置不安全。 我们通常可以通过某些保留关键字的存在(或缺失)来找到不安全的配置。这些配置可以在代码或配置文件中(哦)。
Go 中的 TLSv1 支持
寻找 。
Go 中的证书验证跳过
我们可以禁用 TLS 证书。
如果 InsecureSkipVerify
为 true,我们可以使用可选的 VerifyPeerCertificate
回调来进行自己的检查。最后的防线是 VerifyConnection
,它对所有连接执行并可以终止 TLS 握手。
另一个简单的 Semgrep 规则来查找所有三个关键字:https://semgrep.dev/s/parsiya:blog-2022-03-go-cert-check
。
tls.Config{..., InsecureSkipVerify: true, ...}
等内容。Java 中的外部实体注入
Java 以 外部实体注入 (XXE) 问题
而闻名。大多数 XML 解析库没有安全默认值。我们使用硬编码字符串和语言常量来查找它们。例如,DocumentBuilderFactory
。
现有的 Semgrep 规则做得相当不错,可以消除误报,但不可能找到所有内容。热点规则更容易,可以为手动审查标记所有内容。我使用了 OWASP XML 外部实体预防备忘单
2. 危险函数
每种编程语言、框架和库都有危险函数。 然而,它们的存在并不一定是一个漏洞。您可以说我们不应该使用这些危险函数,我同意,但移除它们并不总是实际的,尤其是在旧代码中。
MD5
MD5
是一个在密码学上已被破坏的散列函数(强调“密码学上”)。尽管如此,我们不能报告每个实例。有些情况下使用 MD5
是完全没问题的。我在现实世界中看到了一些安全的例子:
- 一个自定义的内容管理系统(例如,一个博客)使用 MD5 为图像创建一个标识符。如果您可以编辑博客帖子并添加一个具有相同散列的不同图像,您可以做坏事并覆盖之前的图像。这是无用的,因为您可以使用您的访问权限直接删除原始图像。
- 从 20 位数字用户 ID 生成数据库索引。ID 必须是一个有效的数字。据我所知,不可能使用两个数字生成 MD5 碰撞(准备好被证明是错误的)。
标记
MD5
是一种本能反应。也许您会创建一个工单,并要求开发人员将其更改为 SHA-256“为了确保”。请记住,您会因为要求开发人员花费周期而没有合理的漏洞而受到打击。 “不安全的随机数”如java.lang.Math.random
也类似。它们在非密码学上下文中是可以使用的。关于“每日提示”模块没有使用 CSPRNG(密码学安全伪随机数生成器)的工单是愚蠢的。
C 中的 sizeof(*ptr)
在 C/C++ 中,使用 sizeof(pointer)
而不是实际的对象类型是一个常见的错误。在这个例子中,我们使用 memcpy(dst, src, strlen(src)*sizeof(char*))
,这会导致经典的缓冲区溢出。sizeof(char*)
通常为 4(x86)或 8(x64)字节,而 sizeof(char)
为 1。
#include <stdio.h>
#include <string.h>
int main() {
char dst[20];
char* src = "hello hello";
// seg fault - sizeof(char*) == 8
memcpy(dst, src, strlen(src)*sizeof(char*));
// sizeof(char): 1 - sizeof(char*): 8 - sizeof(source): 8
// printf("sizeof(char): %lu - sizeof(char*): %lu - sizeof(src): %lun",
// sizeof(char), sizeof(char*), sizeof(src));
}
有趣的是,使用 memcpy(dst, src, sizeof(src))
我们会得到一个警告:
warning: 'memcpy' call operates on objects of type 'char' while the size is
based on a different type 'char *'
[-Wsizeof-pointer-memaccess]
memcpy(dst, src, sizeof(src));
我创建了一个规则来使用 pattern-regex
查找代码中所有的 sizeof($TYPE*)
。这将也会搜索注释。我们可以使用 pattern-not-regex
减少误报。尝试扩展 https://semgrep.dev/s/parsiya:blog-2022-03-sizeof-ptr
。
! Semgrep 规则
我们也可以放弃正则表达式,只使用像这样的模式:
rules:
- id: blog-2022-03-sizeof-ptr
pattern: sizeof($OBJ*)
message: Using sizeof($OBJ*) is wrong, did you mean sizeof($OBJ)?
languages:
- c
- cpp
severity: WARNING
Go 中的 text/template
Go 的标准库提供了两个模板包。html/template
执行一些输出编码,而 text/template
没有执行任何编码。在 Web 应用程序中使用 text/template
可能会导致 XSS。我们应该审查并审查 text/template
导入。
Semgrep 注册表中有一个规则。
Go 中的 Unsafe
我尝试了一个不原创的编程语言笑话:
在每个安全编程语言的标准库下面都有一堆不安全的函数。 Go 和 Rust 被认为是一种安全的编程语言,但两者都允许我们通过 Go 的 unsafe 包 和 Rust 的 unsafe 关键字 使用
unsafe
。 我们应该标记所有的unsafe
吗?这取决于行业。我不这样做。游戏开发者喜欢使用巧妙的技巧。找到这些实例很容易。在 Go 中查找import "unsafe"
,在 Rust 中查找unsafe
。Go 的一个示例规则(Semgrep 不支持 Rust,但 Rust 已经很安全了 :p):https://semgrep.dev/s/parsiya:blog-2022-03-go-unsafe 。在 Go 中查找不安全的导入
3. 危险模式
危险模式通常会导致安全漏洞。 将它们视为“通常安全的方法的不安全使用”。
Java 中的格式化 SQL 字符串
Semgrep 注册表中有 一个规则 ,它看起来很吓人,但实际上只是在尝试查找作为 SQL 查询执行的连接字符串。
exec
(和类似)命令也是不错的选择。我们想审查它们并检查攻击者是否可以影响它们的输入并获得命令注入。PHP 中的 openssl_decrypt 返回值
我最近遇到了这个问题。PHP 中的 openssl_decrypt
是一个安全的函数,在成功时返回解密后的字符串,但在失败时返回 false
。如果我们不检查这个边缘情况,我们可能会有一个漏洞。Semgrep 规则 openssl-decrypt-validate
标记这些情况供审查:
硬编码的秘密
假设您将 AES 密钥存储在源代码或配置文件中。这是一个危险的模式。AES 是安全的,不是一个危险的函数,但因为每个人都有权访问代码,所以您已经削弱了它的安全性。 在您的密码散列方案中使用静态盐也是一样的。您已经削弱了(希望)安全的算法。
4. 有趣的关键字
查找特定的变量/方法/类名称和注释中的术语。 这些不是语言关键字,而是上下文概念(什么?!)。
您是否曾经在一个代码库中搜索过 password
来发现密码是如何处理的?它们可能存储在名为 password
、passwrd
或其他变体的变量中。您在代码注释中搜索过 TODO
或 security
吗?
名称中包含 Encode 和 Decode 的函数
weggli
有一个查找名称中包含 decode
的函数的示例。我想审查任何名称中包含 encode
和 decode
的函数。encrypt/decrypt
也是一个不错的选择。这些函数可能会增加我们的攻击面,因为我们正在处理两种不同的格式。解析器错误很有趣!
Semgrep 规则 https://semgrep.dev/s/parsiya:blog-2022-03-encode-decode-function-name
很容易创建(天哪,我太喜欢 Semgrep 了)。我们捕获所有函数到一个元变量 $FUNC(...)
中,然后使用 metavariable-regex
过滤它们。
错误和功能跟踪代码
错误通常在代码注释中提到。例如,如果我在修复工单 BUG-1234
,我会在代码中的那个位置添加一个注释,并提供一些其他信息。对于新功能或合并/拉取请求也是如此。在代码中搜索这些模式以查找功能、已修复的错误、现有错误、变通方法(// BUG-234: hacky way of bypassing a security guardrail!
)和其他有趣的事情。
在我幸运的 。
CVE-2021-1416
页面上没有太多信息。代码讲述了一个更好的故事。VS Code 服务器会在 Windows 上的特定路径上加载来自 node_modules
的代码。如果攻击者能够在这些路径上放置自己的 Node 模块,他们可以实现远程代码执行。为什么 Azure Storage Explorer
甚至要运行这段代码?!
CVE*
、BUG-[number]
和 CL[number]
(CL 代表 Change List
在 perforce 中,它是 git 提交的等效物)。我是如何收集这些的?
我已经解释了我的示例来自哪里。让我们列一个清单:
- 静态分析规则
- 编码标准
- 文档
- 其他错误
- 经验
1. 静态分析规则
浏览不同语言和工具的静态分析规则。我浏览了 Semgrep 的 audit
规则和 weggli 示例。查看 GitHub 安全实验室的 CodeQL 查询
以获取更多内容。虽然不可能在 Semgrep 中复制 CodeQL 规则的一些内容,但可以提取关键字进行手动审查。
为什么是 Semgrep 而不是 CodeQL 呢?简短的答案是 。
您甚至可以使用其他语言的模式并将其适应您的目标。我们刚刚看到了 Java 中的 XXE,但它也发生在其他语言中。搜索 xml + other-language
并看看您能找到什么。
Microsoft 应用程序检查器
和 DevSkim
规则中的关键字很有用。
2. 编码标准
编程语言和开发团队通常有自己的编码标准。一些函数和库被禁止;一些模式被积极劝阻。将这些添加到您的列表中。 您可以找到旧代码、一次性异常(“嘿,我可以在这里使用一次 memcpy 吗?”)以及在代码审查中遗漏的项目。
3. 其他错误
研究错误。 非常少的公共错误伴随着源代码,但 开源拉取/合并请求 非常有用。识别易受攻击的模式并创建规则。不要花太多时间尝试消除误报。我们的目标是找到热点。有时只是标记一个特定的函数就足够了。 阅读其他工程师编写的内部安全错误。审查外部安全研究人员向您的组织披露的错误。获取代码/配置并尝试执行根本原因分析。找出哪个部分是易受攻击的。这有两个用途:
- 您将找到新的模式并了解更多信息。
- 您可以寻找变体。将模式应用于您代码库的其余部分。
工单编号和注释 也是好的起点。如上所述,搜索工单编号
BUG-1234
、注释(CVE-2021-1234
)和其他项目。
总结
我介绍了源代码中 hot spots
的概念。这些是可能包含漏洞但应手动审查的位置。结果很杂乱,因此热点规则不适合 CI/CD 管道和自动警报。
热点的主要受众是安全工程师。我们必须依赖静态分析工具来审查数百万行代码。我们在这里的方法不是科学的,而是全面的。我们依赖于我们的直觉和手动分析。这通常不是机器可以做到的。
我回顾了现有概念的一些实例,并尝试创建了一个轻量级的分类。我们讨论了每个类别的示例和 Semgrep 规则。最后一节解释了我是如何收集想法和样本来找到更多热点的。