《安全代码审查详解》的标题卡,展示一个代码差异面板,其中包含一行原始内容和一行修改内容,其下方有三个带标签的阶段:模式(确定性),然后是“通过”,然后是推理(模型)。
Guides & Insights

安全代码审查详解:审查者在 Diff 中关注什么,以及为什么

作者

Rowan Sterling

发布日期

最新模型 · 20查看全部模型 →
基准测试:Artificial Analysis · 每日更新
返回全部文章

安全代码审查,是带着一个特定目的去刻意阅读源代码:找出那些能让代码做出作者本意之外的事情的途径。这个定义听起来理所当然,直到你把它与大多数团队实际在做的事情相比较——那其实是一次功能审查,旁边放着一份安全检查清单。OWASP 安全代码审查备忘单把界线划得很精确:安全审查之所以不同于功能审查,在于它针对的是输入验证、身份认证、授权、密码学实现和攻击向量,而不是正确性。一个运行得完美无缺的代码差异,仍然可能给攻击者递上一条进入系统的路径,而这两种审查之中,只有一种会去找它。

让这个话题如今而非2020年值得写一篇文章的原因在于,语言模型能够可靠、低成本且持续地读懂 diff——这改变了审查的经济账,却没有改变被审查的任何一项维度。清单比这些模型更古老,也一直没有变。变的是你能负担得起多频繁地运行它。

审稿人实际上在阅读中寻找什么

OWASP 速查表将工作划分为六个领域,而这份清单的有用之处在于,每个领域都是一个待回答的问题,而不是一个类别。类别容易让人机械打勾;问题则能带来发现。

Card 'What a reviewer is actually looking for' listing six dimensions - input and injection, authn and session, authorization, cryptography, business logic, configuration and secrets - each with the question it answers about a diff.

输入与注入。 问题不在于代码是否有清理器;而在于是否有任何内容会到达解释器——shell、SQL、模板引擎、表达式求值器——以及该要求本应在哪一层强制执行。添加参数化查询的 diff 是好的;但如果一个 diff 把参数化查询加进一个其他调用方会传入原始字符串的函数,那这个 diff 只是把问题转移了。

认证与会话。调用者是谁,代码又是如何确认这一点的?反复出现的缺陷并不是密码校验出了问题,而是会话在权限变更后依然存活——提权发生了,令牌却保留着旧的声明,一小时之后那些权限依然还在。

授权。每次都要在每条路由上进行对象级检查。在 diff 中要特别留意的是:新增的路由没有继承其同级路由所拥有的守卫,因为中间件是按路由器注册的,而挂在另一个路由器下的路由根本不会被覆盖。

密码学。 密码原语、模式、密钥长度以及密钥的存放位置。有两个问题能发现大多数真实问题:任何能阅读此代码库的人是否也能读取密钥,以及这个比较是否属于绝不能泄露时序的比较。

业务逻辑。 序列是否可以被重排、重放,或由客户端选定的值驱动。价格、数量、角色或状态转换如果来自客户端,且未在服务端重新推导,那么无论它如何被验证,都构成一项发现。

配置与密钥。这次 diff 交付了哪些本不该出现在代码仓库里的内容,以及这些默认值在代码实际运行的环境中是否安全。一个默认值为 true 的调试开关,是一条关于生产环境的发现,而不是关于该开关本身的发现。

基线审查与差异审查对比

OWASP 区分两种模式,将二者混为一谈常常既会造成精力浪费,也会带来虚假的信心。一次基线审查会全面检查代码库,适用于新应用程序或重大版本发布、接手遗留系统、满足合规要求,或在问题已经出现后进行事后分析。基于差异的审查只查看变更内容,适用于拉取请求、提交、功能完成和持续验证。

这种区分很重要,因为两者产生的工作不同。基线审查以一份待办事项清单收尾;差异审查以一个合并决定收尾。持续运行前者成本高得难以承受,而在一个从未做过前者的代码库上运行后者,正是团队积累虚假覆盖感的方式:每一次变更都通过了审查,而先前存在的 SQL 拼接却原封不动地留在那里。

Screenshot of the OWASP Cheat Sheet Series index entry for Secure Code Review, showing the introduction describing manual examination of source code for vulnerabilities automated tools miss, the baseline-versus-diff review split with their use cases, and a table of contents covering methodology, patterns, techniques, checklists and tools.

技巧,以及每种技巧能捕捉到什么

OWASP 列出了四种审查技术,区分它们的是审查者所依据的证据,而不是审查的彻底程度。代码模式分析关注的是熟悉的形态:危险的函数调用、对不可信输入的反序列化、缺失的长度检查。它速度快,而且这正是确定性工具所擅长的。数据流分析追踪一个值从进入系统到被使用的全过程,这是判断三个函数之前应用的净化处理在此处是否仍然有效的唯一方法。基于威胁的审查从攻击者的能力出发向前推演,这样才能发现两个各自正确的组件之间的缺口。业务逻辑审查读的是工作流而非代码行,追问的是什么样的合法操作序列会产生非法的状态。

这四者中只有第一个适合用扫描器处理,并且只有第一个和第二个适合通过无引导的模型遍历来处理。这并不是反对模型的论据;而是主张告诉审查者你想要哪种技术,因为一个要求“安全问题”的提示词必定会产生模式分析,然后就此停止。

AI 改变了什么,又没有改变什么

关于AI撰写代码的研究让审查瓶颈变得更加突出,而不是制造了该瓶颈。2022年发表在IEEE S&P上的纽约大学关于GitHub Copilot建议的研究发现,约40%的生成代码片段在18个CWE类别中包含可利用模式。2023年发表在ACM CCS上的斯坦福大学关于开发者行为的研究发现了更具后果的影响:使用助手的开发者更确信自己的代码是安全的,同时产生的代码安全性却明显更低。产出更多加上信心更足,就成了审查难题。

语言模型作为审查者改变了那个等式中的一个变量——吞吐量。它可以阅读每一个拉取请求,成本按 token 而非按席位计算,并且可以明确地向它提出上述六个问题。它不改变这些问题,也不具备使审查值得信赖的两个属性:确定性和执行代码的能力。同一个 diff 被审查两次可能产生不同的发现,严重程度在多次运行之间会发生变化,而且审查者无法证明被标记的路径是可到达的。

Screenshot of the OrcaCode Review product page on OrcaRouter headed 'Open engine. Open prompt. Open bill.', describing a pull-request reviewer whose rules and merge gate run on GitHub under an MIT licence and are billed per token, with an example finding about an idempotency key that still varies in time.

OrcaCode Review就是那个想法的产品化形态,值得仔细审视,因为其设计选择揭示了失效模式。一份审查若能把失败案例隔离成具体、可复现的评论——这个键仍会随时间变化,因此一次重试的扣款会生成一个新键,并可能造成重复扣款,这里就是那一行改动——那它就是在做审查者该做的工作。一份只报告“潜在安全问题”却不提供复现的审查,是在做模式分析,其精度还不如模式匹配器。而一个以开放许可证发布的规则与门禁引擎,正在提出一个值得核查的隐含主张:审查者的判断也应当是可复核的。

行之有效的双层安排

给定一个确定性扫描器和一个概率性审查器,实践中能长期存续的分工是让各自做真正擅长的工作。扫描器在每次推送时运行:几秒钟完成,不用模型,没有波动,能捕获密钥、危险 API 调用、不安全反序列化和幻觉导入。审查器在每次拉取请求时运行:几分钟完成,由模型支持,能捕获字里行间的逻辑——授权检查是否在获取数据之前运行、幂等键是否绑定到稳定值、新路由是否继承了防护。

接下来是人们跳过的那一层:门禁。不阻断合并的发现只是通知,而通知是周五才看的。门禁必须窄,而且必须绝对——关键发现阻断,其余一切只评论——豁免必须写下来,因为门禁的价值不在于它阻止了第一个发现,而在于它让每一次绕过都可见。

审查的是变更,而不是代码库

diff 审查的实用准则是套简短流程,其价值在于一个下午就能教会。阅读 diff 时,要看它增加到攻击面的内容,而不是看它改变了什么。对于每个新入口点,沿着输入再跟进一步,越过 diff 结束的位置——漏洞通常就在它调用的函数里,而作者没读过那个函数。对于每条新路由,检查守卫注册。对于每个新依赖,检查它是否存在,以及是否有别人发布过这个名称。对于每个新配置值,问它在实际部署的地方默认是什么。

这些都不要求很懂这门语言,因此可以推广到并非安全专家的评审者。但它确实要求评审者查看的是差异,而不是文件,因为整个方法都依赖于知道哪些内容是新的。

诚实的限制在于:差异审查无法发现差异出现之前就已存在的问题。如果这个应用从未做过基线审查,那么可用的最高价值安全工作并不是找一个更好的审查者,而是针对涉及金钱、身份和文件访问的区域,带着上述六个问题,对代码库做一次刻意的全面检查。在那之后,每一次后续的差异审查保护的都已经是至少被检查过一次的东西。在此之前,每一次差异审查保护的都还是一个未知对象。