引子
随着DevOps模式的落地,快字当头。研效提速也意味着出现安全漏洞的数量和概率随之上涨。过去安全风险的管控主要依赖于DAST类技术,即:采用黑盒测试技术,对待检查目标发起含检查用例的请求。DevOps给这一模式带来了挑战,安全检查速度慢、周期长,容易给业务带来干扰,一定程度上有阻碍业务持续交付的风险。另据Capers Jones的研究结论:解决缺陷的成本,在研发流程中越靠后越高。
因此,安全机制的左移在开展DevSecOps建设时就变得更为重要了。左是指软件开发生命周期的早期,也就是设计、编码阶段。发生安全漏洞研发的早期阶段存在哪些问题?我们认为可以总结为三类:
开发人员不了解安全编写代码的知识
开发人员有一定安全意识,但因为赶进度或疏忽遗漏编写安全机制,或是错误地实现安全校验逻辑
代码编写是否安全仅凭开发人员自觉,缺乏检查和卡点机制
调研业界理念与实践基础上,团队进行了安全左移建设的探索,主要包括三个机制:面向开发人员的代码安全指南、默认安全框架组件、嵌入基础设施的代码安全检查。作为前述项目的参与者,我们想与业界同行分享交流其中的一些思考与经验。
I. 代码安全指南
1.1 背景
**首先是开发人员缺乏安全编写代码知识的问题。**在开展日常安全运营过程中,常常会遇到如下与之关联的挑战:
一个业务被发现问题,如何把这种“踩坑”的经验沉淀下来,分享给其他业务以及新加入公司的同事?
如何帮助开发人员建立起安全编码意识,实现代码写出来就没有漏洞?
当检出安全漏洞时,如何给予开发人员详细、可操作的改进指引?
安全编码意识和漏洞修复指引,两份材料是否合二为一?
综合上述背景,去年梳理了一份统一代码安全指南。从开发人员视角讲述安全注意事项,并配套了丰富的代码示例。覆盖常见的8门编程语言,包括:C#、C,C++、Go、JavaScript、Java、Objective-C、PHP、Python。
1.2 设计理念
代码安全指南的内容呈树状结构展开,共分5层,如下:
1.2.1 语言
每种语言面临安全的风险种类不同,需要分别开展详述。如:go和javascript对比,go就不存在原型链污染的问题。同时,由于公司内的代码风格规范亦分语言展开,安全规范采取相同的分语言方式能保持整体的连贯性。
1.2.2 端
这里的端是指不同的终端,如:Web、安卓客户端、iOS客户端、PC客户端。实践过程中,将内容按端区分的原因有:
1、同一门编程语言,用在不同的终端应用开发,其面临的风险类型和数量有着天壤之别。例如:JavaScript应用于前端页面开发时,面临的主要风险是DOM XSS;但JavaScript亦可依托Node.js进行Web后端接口开发,如果编码不当,则存在命令注入、SQL注入等风险。
2、大型互联网公司内,项目开发采取“流水线”化作业,分工往往精细明确,将不同端的场景作为主干目录,更便于开发人员检索、快速了解编码安全知识。
1.2.3 场景
通过复盘历史漏洞,安全风险可按成因粗略归为两类:
1、代码漏洞,是指代码编写时,因不安全的API使用和逻辑编写产生的安全风险。
2、运维漏洞,是指代码的运行环境、配置和依赖等系统运维相关的安全风险。如腾讯蓝军分享的《浅析软件供应链攻击之包抢注低成本钓鱼》一文,涉及的安全风险本质上是是:部分语言依赖包管理,当部分企业私有软件包仅在公司内部软件源注册时,攻击者就可以在外部公共软件源上抢注。如果公司内员工使用包管理软件拉取时,未配置公司镜像源时,就会拉取到攻击者抢注的恶意包。
1.2.4 功能
在对内、外部发现的漏洞进行复盘过程中,我们发现安全风险与业务场景高度相关,例如:
漏洞 | 业务场景 | 成因 |
---|---|---|
SQL注入 | SQL查询 | 拼接SQL查询语句,未做参数绑定,且未对参数值做校验 |
SSRF | 资源请求 | 未判断网络资源请求的目标是否属于内网 |
命令注入 | 命令执行 | 拼接命令、未对拼接入命令的参数值做过滤/校验 |
由于代码安全指南的目标受众是开发人员看的。在撰写指南过程中,我们尝试将漏洞转化为功能场景,作为主干目录。由于与具体的业务场景关联,在开发时能更容易想起相关的注意事项,由此可降低认知、学习成本。
1.2.5 API/sink点
对于开发人员来说,
各类API是程序代码的基础组成部分。对安全团队来说,API也就是编写安全检查策略要收集整理的sink点。
1、为什么要在代码安全指引中,枚举API/sink点?
对开发人员来说,API是实现业务逻辑时,高频接触对象。通常安全漏洞往往可归因为API的错误使用。对安全工程师来说,sink点是编写安全策略、组件是非常重要的一部分,直接决定了SAST系统的扫描能力。
业界也不乏类似的探讨,如Google在ICSE ’21的论文《If It’s Not Secure, It Should
Not Compile: Preventing DOM-Based XSS in Large-Scale Web Development with API
Hardening》中,阐释了Google加固前端组件,使其Web页面能天然“免疫”DOM
XSS漏洞的实践过程。
其中主要的思路是对.innerHTML等容易被误用产生安全问题的API做了加固,封装为前端组件中新的函数对象。形成了JavaScript公共库https://github.com/google/closure-library,并设计了对应的编译时检查工具。 自2018年在其内部逐步推广后,截止2020年Q2外部向Google报告全部漏洞类型中,DOM XSS已由原先的20%降低至2%。
在上述实践过程中,最为重要的一步是:XSS sink点的提取,也就是易被误用产生安全漏洞的API。原文“We would like to mention that many other XSS counter-measures, such as data-flow analyses, also need to identify areasonably comprehensive set of XSS sinks to be effective. Therefore, enumerating the sinks are somewhat orthogonal to API
hardening”,指出sink点的提取是开展API加固极为重要的一步。
与上述实践类似,我们认为在代码安全指南中,清楚地列出容易被误用的API,对日常开发和安全策略建设均有帮助。
2、如何确保所枚举API的完善性?
这也是上述论文中抛出的一大难点,原文提到:“It is unrealistic to cover all browser-specific XSS sinks because many browsers have undocumented behaviors. As our best effort, we work with the developers of some mainstream browsers to stay informed about new browser features that may have security implications.”。文章指出,这是一项非常有挑战的任务。
编写代码安全指引时,我们亦遇到了类似的挑战,采取的解决思路是:
1.整合各语言、组件、框架文档中的最佳安全实践。在编写安全指南初期,我们重点参考了CWE、OWASP等材料。
2.结合内、外部已知的漏洞案例的复盘、抽象,做校对补充。
3.举一反三,推导鲜有提及的风险点。例如,使用jQuery的页面,会因为不安全地使用.html()方法产生DOM XSS漏洞。那是否还有其他函数有同样的风险呢?通过查阅jQuery的开发者手册https://api.jquery.com/html/发现,.html函数接受的入参类型为htmlString和function。
官方文档给出的示例如下:
$( "div.demo-container" )
.html( "<p>All new content. <em>You bet!</em></p>" );
$( "div.demo-container" ).html(function() {
var emphasis = "<em>" + $( "p" ).length + " paragraphs!</em>";
return"<p>All new content for " + emphasis + "</p>";
});
因此可以得出产生漏洞的原因是:从用户可控来源获取切未经过滤的值,可定义为htmlString类型并经.html()写入页面,进而产生XSS漏洞。顺藤摸瓜可以整理出:
*.append()、.prepend()、.wrap()、.replaceWith()、.wrapAll()、.wrapInner()、.after()、.before()*等API存在同样的因误用产生安全风险的可能性。最终,针对上述场景下的指引要求描述如下:
除列出所有风险API,让开发人员在使用时,脑海里能对快速关联到指引要求外,在编写指引时,我们约定还采取了两种额外的表述注意点,来保证指引内容的可操作性:
a. 限定产生安全风险的开发场景,更明确;
b. 明确给出安全漏洞的规避方式,或是提供可选的“开箱即用”式安全方案。最终,效果如下:
1.3 效果与不足
通过上述的思路,撰写代码安全指南并配套在线学习课程和考试,我们试图解决开发人员不了解如何安全地开发的问题。当前,该方向仍存在两项挑战:
1、补充完善sink点的工作长尾且任务量大,需要随着内外部发现的漏洞、编程语言&框架的迭代不断迭代。为此我们决定将上述代码安全指南通过Github开源,**希望和社区携手,一道维护完善。**项目地址:https://github.com/Tencent/secguide
2、代码安全指南只是安全左移建设的第一步,还需解决的挑战有:
开发人员有一定安全意识,但因为赶进度或疏忽遗漏编写安全机制,或是错误地实现安全校验逻辑
代码编写是否安全全凭开发人员自觉,缺乏提示、检查和卡点机制
II. 静态代码安全检查
2.1 现有技术
“代码编写是否安全全凭开发人员自觉,缺乏提示、检查和卡点机制”的问题,解决方式是:静态代码安全检查解决。
要对代码安全规范扫描,我们首先需要考虑的就是将源码表示成一种方便检查的形式,然后选择合适的方式进行扫描。常规的源码表示方式以AST(抽象语法树)和IR(中间表达)为主:以C++ 为例, clang可以将C/C++源码转换为clang AST和LLVM IR。
一般而言,IR是从AST经过层层转换而来,所以会比AST拥有更多信息。但相对的,每种语言一般会有自己不同的IR,处理起来会相对比较困难。而AST的形式则简单直观,操作起来也比较容易。
源码
Clang AST
LLVM IR
根据选择的源码表示不同,有不同的方式做代码检查:
方式一、AST匹配
第一种方式是在AST上做检查,既然源码被表示成了树的形式,我们就可以遍历AST,使用一些pattern去检查代码规范中的问题。如:Go语言代码安全指南中的1.7.2条目(CSRF防护)
// good
import (
"net/http"
"github.com/gorilla/csrf"
"github.com/gorilla/mux"
)
func main() {
r := mux.NewRouter()
r.HandleFunc("/signup", ShowSignupForm)
r.HandleFunc("/signup/post", SubmitSignupForm)
//使用csrf_token验证
http.ListenAndServe(":8000",
csrf.Protect([]byte("32-byte-long-auth-key"))(r))
}
对于该样例,我们需要做两个检查:
检查http.ListenAndServe参数中是否存在CSRF防护相关配置
检查该配置的正确
具体实现上,我们可以遍历该文件的AST,寻找到http.ListenAndServe调用所在的节点,遍历它的参数,观察是否存在CSRF防护相关的参数,再对参数的内容正确性做检查。
通过使用AST的方式我们能方便快捷的进行相关规范条目的验证。但对于一些条目,例如C++的1.2条目:
// Good
std::string cmdline = "ls ";
cmdline += user_input;
if(cmdline.find_first_not_of("1234567890.+-\*/e ") == std::string::npos)
system(cmdline.c_str());
else
warning(...);
很显然这里如果要判断它违反规范需要满足两个条件:
cmdline含有用户输入
没有对cmdline做相关检查
如果说第二个条件还能通过模式匹配的方式检查的话,第一个条件就很难通过模式匹配的方式检查了,所以我们需要另一种检查方式。
方式二、污点分析
污点分析是更为常用的检查安全漏洞的方法。通过标记外部输入点、基于程序数据流观察数据是否能到达危险函数(如:SQL查询,命令执行等函数)来检查安全漏洞。
要实现污点分析,有三个核心的关键点:
source点:表示外部输入,如web输入,文件输入等。
sink点:表示危险的函数调用或者变量赋值,如system等函数
sanitizer:表示过滤点,经过过滤点的数据将被取消标记。
以下面这段代码为例,污点分析从user_input一路进行数据流追踪(一般因为source点的数量远远大于sink点的数量,会反过来从sink点开始回溯以提升效率),中间的检查就是sanitizer,这时候会取消标记,因此该漏洞并不成立。
使用污点分析,由于是通过数据流追踪的方式得到漏洞,我们能得到程序相对精确的漏洞触发流程,便于研发二次检查。同时因为依赖手动配置source点、sink点和sanitizer,会有部分漏报。而且研发往往会采用不同的过滤方式,不同的写法,导致sanitizer并不能很好的确定下来。在大型项目中,组件往往分布于不同仓库,加上许多自定义的通信框架,完整的程序数据流会很难得到。
综上我们可以总结出两种不同的扫描方式各自的优缺点:
方法 | 优点 | 缺点 |
---|---|---|
AST匹配 | 开发简单 速度较快 | 模式匹配能力有限 |
污点分析 | 完整的漏洞触发流 能针对特定条目做检查 | 维护成本较高 速度相对较慢 会有部分漏报 |
2.2 工具调研
在初步了解了扫描方式后,就要对社区工具做调研。针对单一语言的工具写起来就太多了(各种语言的lint),这里主要提两个工具:Semgrep和CodeQL。一是这两个工具支持语言都不止一个,二是在各自技术领域它们都比较有代表性。
2.2.1 Semgrep
Semgrep是一款基于Facebook开源SAST工具pfff中的sgrep组件开发的开源SAST工具,目前由安全公司r2c统一开发维护,提供强于grep工具的代码匹配检索能力。
其核心技术原理可以用下面这张图概述:
Semgrep支持两种类型的代码模式匹配。一种是基于AST的匹配,使用tree-sitter
parse各种语言的源码,并将它们转换成generic AST(Semgrep内的一种通用AST格式),然后再使用规则匹配。举个例子,如果要检索代码中使用strcpy的代码行,可以编写如下ripgrep命令:
rg --quiet --stats strcpy -tc
但类似如下的代码也会被匹配到,实际上它仅为注释,并没有真正调用strcpy:
45:// string.h is not guaranteed to provide strcpy on C++ Builder.
而运行如下semgrep命令,即可基于AST搜索,实现更精准的匹配。其中 … 是semgrep提供的代码模式匹配语法 —— 省略号运算符(Ellipsis
operator),用于代表若干参数、语句或字符:
semgrep -e "strcpy(...)" --lang=c .
另一种模式是generic pattern matching,使用一个通用文本parser(spacegrep),通过分词和特殊字符的识别来做代码模式匹配。相比基于AST的匹配,该模式能力较弱,但拓宽了工具对各类编程语言的覆盖能力。例如对YAML、ERB、Jinja等语言,就可以使用该模式进行检查。假设有一个配置文件如下,想要检索其中不正确的“allowed_origins”配置:
resource "aws_s3_bucket" "b" {
bucket = "s3-website-test-open.hashicorp.com"
acl = "private"
cors_rule {
allowed_headers = ["*"]
allowed_methods = ["PUT", "POST"]
allowed_origins = ["*"] # <--- Matches here
expose_headers = ["ETag"]
max_age_seconds = 3000
}
}
尽管还不支持解析该文件格式的AST,可以基于spacegrep提供的能力,编写如下规则对代码库进行检索:
但该种匹配模式下,有时无法区分出是真实的API调用,还是代码注释。两种模式对同一代码片段匹配效果,比对如下:
综上,Semgrep的特点如下:
安装便捷。通过包管理工具安装cli,即装即用。
规则上手、编写简单。采用yaml配置文件编写扫描规则,语法简单但表现能力相较于传统grep类工具更强大。
引擎本身和规则集均开源,且支持的规则集丰富,已经开源包含总计有1000+条规则。
扫描速度快。直接基于AST/文本匹配,工具效率会相对较高。经测试,扫描速度可达到2~10w行代码/秒。
支持代码自动修复替换。通过AutoFix语法可自动修复存在安全风险的代码。
易于集成,官方已给出了一系列和CI集成的配置,涉及14款CI平台,如Jenkins、Gitlab、Circle CI等。
2.2.2 CodeQL
CodeQL是Semmle公司推出的一款静态代码分析工具(以前叫Semmle QL),后被Github收购,并成立了Security Lab支持Github的开源代码安全检查能力。
相对于Semgrep,CodeQL走的是深度分析的技术路线,核心技术原理是分析源码并将其转换成代码快照,然后通过它自己的QL查询语言做代码查询来达到扫描的目的。
如上图,先提取不同语言的源代码文件到代码快照,然后将查询规则(.ql文件)编译成CodeQL内部的查询形式,再对代码快照进行查询。
其规则形式如上(一部分),编写起来会比较复杂。CodeQL的优势是支持完整的污点分析和过程间分析,相对不可避免的,分析时间会大大上升。
值得一提的是:两款工具都对策略规则写法做了统一,能较好地解决跨编程语言检查的需求。设想一下:如果用每种语言的lint编写检查规则,不同lint间的规则语法不一致,将会带来比较高的维护门槛。
2.2.3 比对及选型
通常,编写代码涉及三个环节 — 编码、代码托管、CI/CD,开发人员在各阶段对检查工具和结果的期望和要求是不同的。可以简单概括并比对如下:
编码 | CI/CD | 托管 | ||
---|---|---|---|---|
涉及系统 | IDE | 编译器 | 持续集成系统 | 源码仓库 |
特点 | 检查提示快; 修改成本低 | 检查提示快; 修改成本低 | 检查提示较快; 有一定修改成本 | 检查提示慢; 有一定修改成本 |
开发人员心理 | 接受度高; 容许一些错误的提示 | 接受度更高(* 设想一下,如果写错了、编不过,开发人员一定返回去修改); 容许一些错误的提示 | 接受度较高; 有一定准确率的要求 | 接受度低; 对准确率要求高 |
两款代码检查工具可简单比对总结如下:
工具 | 关键词 | 应用场景 | 语言覆盖 | 扫描速度 | 规则定制难度 | 是否支持自动修复 | 数据流跟踪能力 |
---|---|---|---|---|---|---|---|
Semgrep | 轻量、快速、AST | 代码批量检索与替换 风格/安全规范检查 辅助漏洞检测与变体分析 | 已稳定支持9种语言,包括:Go、Java、JavaScript、JSON、Python、Ruby、TypeScript、JSX、TSX | 较快 | 上手较为容易 | 是 | 弱 |
CodeQL | 深度、专业、基于数据流/控制流的分析 | 深度漏洞及其变体检测分析 | 已支持7种语言,包括:C/C++、C#、Go、Java、JavaScript、Python和TypeScript | 较慢 | 有一定上手门槛 | 否 | 强 |
2.3 结论与实践
在编写代码的不同阶段,开发人员对检查工具和结果的预期和要求各不相同。在不同编码阶段采取的静态代码安全检查方案可概括如下:
1、本地编写阶段
在本阶段,对工具有一定要求:速度快,容错性要高。而且往往这个点代码还未写完,工具的检查错误可以快速得到修复。这时候AST模式匹配就会是一个很好的检查方式。在编码阶段,可以使用semgrep等类似lint、grep工具,封装为IDE插件。
实践过程中,还可以尝试引入一些更激进的策略,如:提示并要求修改不安全的SQL查询拼接逻辑。开发人员可根据提示快速判断,如果风险确实存在,即借助自动修复功能修改代码,进而在代码构建前收敛安全风险。
2、构建部署阶段
这个阶段代码已签入代码仓库,类似semgrep或CodeQL的方案可封装为流水线原子,提供给开发人员使用,并配套提供安全卡点(质量红线)机制。如果检查出安全漏洞,应给出详细的漏洞触发路径,并提示风险关联的代码安全指南条目来引导修复。
3、日常检查阶段,代码仓库检查
该阶段一般为纯旁路的检查,时效性可以适当放宽,可进行一系列复杂的分析。但需要注意的是,由于代码仓库是纯静态代码,如果工具对编译有要求(如Clang,CodeQL的部分语言),在该阶段检查应注意编译环境的适配。
值得一提的是,代码安全规范/指南也在编写策略的过程中扮演非常重要的角色。如果公司内有一份比较清晰、完善的规范,能详细地列出各类容易出错的API,编写检查规则会容易许多。
III. 默认安全的框架组件
3.1 背景
当有了一份代码安全指南和配套的静态检查机制后,开发人员仍需要自行在项目中引入或实现安全校验逻辑。这时候就会遇到“开发人员有一定安全意识,但因为赶进度或疏忽遗漏编写安全机制,或是错误地实现安全校验逻辑”的问题。
深挖背后的原因及“痛点”,我们认为可以归纳为如下三点:
全凭安全意识不靠谱。**即便是安全工程师也会犯错,需要将安全指南的要求转化为开箱即用式的安全组件。
重检查,轻具可操作性、便捷的解决方案。**检查工具聚焦于发现问题,但开发人员在解决问题的时候,如果没可操作的解决方案,修复工作往往难以推动。
研发效率与质量。不同业务重复在写安全校验逻辑,不仅浪费人力,还容易出错。
通过分析业界相关理论与实践,借助公司建设统一后台框架的契机,我们开展了开展默认安全的框架组件建设。
3.2 分析与解决
3.2.1 业界理论与实践
1、嵌入框架的安全组件。Google在《Building Secure and Reliable Systems: Best Practices for Designing, Implementing, and Maintaining Systems》一书的第12章中分享了其内部在代码编写阶段开展的安全左移建设实践。可概括为:
“越早越好”。
应该在软件设计初期就考量安全、稳定性问题。否则,越往后代价越高,过程会很痛苦。(笔者注:本质上就是安全左移)
“安全意识教育很重要,但不是银弹,即便是安全工程师也会犯错”。
依赖安全意识教育,效果不佳。这是因为,业务开发阶段,关注点往往是功能的实现,要兼顾安全和稳定性设计方面的权衡,会很难。因此,应侧重关注安全机制、工具的建设。
Google团队认为,安全能力嵌入框架大有裨益,如下:
规范化与一致性,复用最佳实践。某些功能容易出问题,通过框架封装,能减少由业务各自实现产生的风险
提升研效。业务逻辑与通用功能抽离,专注业务逻辑开发,无需关注其他细节。不仅能减少代码审阅的精力,也能缩短修复问题的时间。
降低修复成本及时间。虽然框架不能保证预防、解决所有安全问题,但出现问题时,只需要在“一个点”集中解决
2、Security by Design。USNIX15’的议题《Preventing Security Bugs through
Software Design》中抛出了与众不同却巧妙的观点:写出漏洞不一定是开发者的问题,而可能是API设计得容易出错。以SQL注入为例,尽管大部分语言、组件和框架都提供了参数绑定(参数化查询)的功能,安全规范也有不断强调。但开发人员仍非常容易写出带SQL注入问题的代码。
为解决前述问题,提出了一种名为trustedsqlstring的机制,从接口设计层拒绝拼接SQL查询语句传入。以Go语言例,各类参数需要标注静态类型的机制。通过修改SQL查询组件导出操作函数的入参类型,可以从API设计机制上禁用查询拼接,仅允许通过占位符动态拼接参数值进入查询语句。
虽然Google仅分享了针对XSS和SQL注入做组件加固的方案,并不足以覆盖所有应用漏洞。但核心思路仍具有借鉴价值,可归纳为:基于白名单模式,对现有组件做安全加固,使安全机制在默认情况下能生效。
3.2.2 安全组件方案分析
通过整理汇总,可以把现有的安全过滤方案分为三类:校验、过滤和组件。
示例 | 优点 | 缺点 | |
---|---|---|---|
校验 | Spring 3 框架内置数据校验机制 (Java) | 不会改变入参原意,属于上上策 业务修复漏洞的首选方案 | 部分漏洞类型无法通过参数校验解决,如:SSRF 很少有业务做或能做好校验 |
过滤函数/库 | 各类安全过滤库 | 灵活 | 错用、忘记调用,都会导致漏洞 检查、统计成本高 |
带默认安全机制的功能组件 | SQL组件内置的参数化查询特性 | 安全与功能特性的深度融合 | 错用 |
从上述对比来看,组合采用校验,和带默认安全机制的功能组件(融入Security by Design设计思路)两个方案是最优解。
3.2.3 落地案例
数据校验是几乎每份安全规范类文档会囊括的要点,但实际在业务研发过程中,落地情况并不理想。
一方面,引入数据校验有一定成本和门槛,导致该逻辑往往会被省略;另一方面,即便是编写了数据校验逻辑,其准确性和安全性也鲜有检查。结果是:我们在内部安全工单系统中,能看到许多开发人员在漏洞处理结论时备注“未做数据校验,已经通过添加相关逻辑解决”。据内部复盘数据统计,这类可通过数据校验规避的漏洞约占历史Web漏洞安全工单数量的70%~80%。
为此,我们为公司级RPC框架引入了一套Validation组件和检查、卡点机制,并取得了不错的效果。公司级RPC框架使用Protocol Buffer作为IDL,根据设计理念,Protocol Buffer原本就带有一定数据校验类似的特性。但经分析,其不足以支撑业务和安全的需求,例如:校验入参是否为空?传入的是否是字母数字组合,这些需求是PB原生的能力无法支持的。因此,需要引入第三方数据校验组件补充这部分能力,当前主要有两类方案:
方案1、在代码中引入validator,并编写校验规则
方案2、在IDL文件中编写校验规则并自动生成代码的protoc插件
通过分析对比,最终选择了方案2 —— 开发人员在定义proto文件时即在拓展字段中定义好校验规则,过程如下:
同时,通过对历史漏洞的复盘分析,我们发现许多漏洞是string类型的字段未限制字符集产生的。如:id参数功能上仅允许数字传入,但未做校验,允许传入`)(‘”等字符,带入经拼接产生的SQL查询语句,最终产生了SQL注入漏洞。于是,我们在内部代码安全指南中添加了明确的要求:请求(Req)消息体中的String类型字段必须限定格式/字符范围+长度。
另一方面,我们还与基础设施开发团队合作,将上述组件嵌入了proto管理平台默认提供的文件模版中。开发人员几乎可以无额外成本,快速上手使用这套机制:
此外,我们还设计了静态安全检查和卡点机制,并引入上述平台,来保证数据校验逻辑的准确性和安全性。开发人员在保存proto文件时,能立即获得提示并及时修正存在的安全隐患。
综上,数据校验本身是一项业务需求,通过降低开发门槛、给出安全要求并配套自动化检查和卡点机制,在框架层提供组件让每个业务模块都能做好数据校验。基于前述思路建设的组件还有不少,包括:DB组件、网络资源请求组件等。这些组件不是自成一体的安全过滤函数库,而是与某项特定功能需求结合的特定组件。
3.3 效果与小结
结合对业界现有理念与实践的分析,我们基于代码安全指南在公司级框架上建设了一系列“默认安全”的组件,并设计了一系列检查工具嵌入在统一的研发基础设施中。
理想情况下,组件是和代码安全指南高度对应的。举个例子,开发人员在Go项目编写资源请求功能时,无需在每个项目中自行实现SSRF防御逻辑,只需引入组件即可快速实现业务功能,又确保安全。
由此带来的收益是:
更高的业务接受度,安全机制内置在功能组件中,使用几乎无感知,且会被当作一种最佳业务实践口口相传,逐步获得高覆盖度
代码安全检查更准确方便,安全过滤特征范围缩小,检查工具对
研发效能的提升,开发人员无需自行、重复实现安全校验/检查代码
当然,上述工作也给安全和基础组件研发团队提出了挑战:既要对应用安全风险有全面且深入的了解,在开发安全组件时,还要兼顾功能灵活性(业务需求)和安全性(安全需求)。
IV. 总结
综上,借助代码安全指南、默认安全框架组件、嵌入基础设施的静态代码安全检查三项机制联动,我们试图在软件研发生命周期的最初阶段收敛漏洞。
在上述框架下,研发和安全人员日常开展工作的模式为:
研发人员
阅读学习代码安全指南、安全组件文档
业务基于统一框架、组件设计功能,聚焦于业务逻辑
使用自助安全检查工具,在DevOps各环节检查漏洞,漏洞修复指引会链接回代码安全规范,形成闭环。
安全人员
有新的漏洞进来 -> 分析提炼漏洞模式(变体分析)-> 沉淀至代码安全规范
基于代码安全规范编写检查规则(浅层和深层)
推出对应的安全组件
在安全左移探索过程中仍有许多机制尚待优化,我们希望通过逐步开源上述解决方案,与业界一道丰富安全左移的理论和实践。
上述探索过程中,获得了基础设施研发团队(框架、持续集成平台)、业务侧研发安全团队的支持,在此献上诚挚的谢意。
参考资料
[1] Kern, Christoph. “Preventing security bugs through software design.” (2015).
[2] Wang, Pei, Julian Bangert, and Christoph Kern. “If It’s Not Secure, It
Should Not Compile: Preventing DOM-Based XSS in Large-Scale Web Development
with API Hardening.” 2021 IEEE/ACM 43rd International Conference on Software
Engineering (ICSE). IEEE, 2021.
[3] Adkins, Heather, et al. Building Secure and Reliable Systems: Best Practices
for Designing, Implementing, and Maintaining Systems. O’Reilly Media, 2020.
[4] 轻量级开源SAST工具semgrep分析
来源:freebuf.com 2021-05-31 21:48:23 by: 腾讯安全平台部
请登录后发表评论
注册