DevSecOps实战
上QQ阅读APP看书,第一时间看更新

3.2 安全编码

在开发人员了解了需求和设计,以及拥有一定的安全意识和了解代码规范之后,就要进入编程阶段。编程过程中有两个最主要问题,容易导致安全漏洞和风险的出现。第一个就是有关输入输出参数处理不当导致的漏洞,在大部分场景下该类漏洞占比常常达90%以上,另一个是在业务逻辑编码中逻辑实现出问题导致的逻辑漏洞。对于几乎所有企业来说,如何更好地进行安全编码是一个重要的问题。而从实践角度来看,如下几个方面有重大的参考价值。

3.2.1 默认安全

在这里笔者不想争论诸如“世界上最好的编程语言是哪个?”之类的话题。但仅从安全角度来说,某些场景下一些内存操作不安全的编码语言确实往往会产生更多的安全风险和攻击利用,比如从2020年开始,微软公司已经在博客中讨论他们使用Rust语言重写部分系统组件甚至是内核模块,谷歌公司也在考虑使用一种内存安全语言重写Chrome浏览器系统。这些行为可能也是一种体现,Windows操作系统内核以及Chrome软件大部分代码采用C++完成,这种语言本身的内存不安全问题导致的安全漏洞和攻击事件可能会占到整个产品安全风险的70%以上,如果换一种编程语言就可以解决已出现的70%以上风险,这好像是一件值得考虑并实施的事情。在硬件以及云基础设施大规模发展的今天,一些默认内存安全的语言已经并将继续获得更大的发展,比如后台开发领域的Java、Go,Web开发领域的Python、JS等。在互联网和金融等领域,笔者很看好JS语言和Go语言的发展。

3.2.2 安全编码规范

大部分公司都会有一些安全编码规范,但这里容易犯一个错误,即绝大部分的安全编码规范都是由安全人员编写,并且不太会考虑编码语言,这其实是站在安全人员和安全领域的视角下写的。但是很重要的一点是,我们首先要弄清楚安全编码规范到底是给谁看的,它的目标受众显然不是安全人员,而应该是开发人员。一个写后台C++服务的开发人员和一个写前端JavaScript代码的开发人员都看同一个安全编码规范,这显然不是一个好的选择。所以,强烈建议由安全人员和开发人员一起编写和完善公司内常用编码语言的安全规范,并且要更多站在开发人员的视角来写,这样更容易理解和执行。下面举个例子来感受一下两者的不同。

  • 安全人员视角

XXE漏洞的防御方法:读取外部传入XML文件时,XML解析器初始化过程中设置关闭DTD解析。

  • 开发人员视角(比如Java的安全编码规范)

XXE漏洞的防御方法如下:

1)若使用javax.xml.parsers.DocumentBuilderFactory,则使用如下代码关闭DTD解析避免漏洞。

DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
try {
    dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
    dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
    dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
    dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
    dbf.setXIncludeAware(false);
    dbf.setExpandEntityReferences(false);
    ……
}

2)若使用org.xml.sax.XMLReader,则使用如下代码关闭DTD解析避免漏洞。

XMLReader reader = XMLReaderFactory.createXMLReader();
reader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
reader.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
reader.setFeature("http://xml.org/sax/features/external-general-entities", false);
reader.setFeature("http://xml.org/sax/features/external-parameter-entities", false);

3.2.3 安全函数库和安全组件

很多时候,某些场景下很难使用简单的安全编码规范描述来直接指导开发人员正确完成编码。因为对于一些漏洞,除非直接提供过滤函数库甚至是安全的组件,否则非常容易犯错。哪怕是编码老手都容易犯错,比如SSRF漏洞的防护函数会被轻松绕过。另外还有很多攻击利用方式(比如dns rebinding等),这些并不常为开发人员所了解。此时最好的方式就是直接提供一系列安全函数库(比如公开的OWASP的ESAPI等)或者是内置了安全特性的组件,比如针对请求资源的业务代码很容易产生SSRF问题,可以直接提供一个封装好的请求库,里面默认完成一个安全的请求。

3.2.4 框架安全

再进一步,成熟的研发团队一般都会有自己的开发框架,可以避免每个开发者从头做起,把一些网络请求和复杂过程封装起来,开发者只需要补充业务逻辑代码即可,极大地提升了开发效率。那是不是也可以由安全团队和开发团队一起在使用的框架中内置安全特性?这其实是一个非常好的思路,也是值得投入资源来做的事情。举个例子,比如一些公司使用PHP或Python来做Web网站开发,如果你使用最新的CodeIgniter框架或Django框架,那么你会发现预防诸如CSRF漏洞只需要修改一个配置即可,其中也有一些数据库安全组件内置在框架中,可以直接使用,能够有效地避免低级错误。

考虑到参数的非正确校验处理往往是产生漏洞的大多数原因,因此有必要在框架中集成一个专门的参数校验的机制,类似于Java Hibernate中的Validator机制等。另外很多互联网公司会使用谷歌的Protocol Buffer来标定接口协议,有一个很有创意的方案叫protoc-gen-validate(https://github.com/envoyproxy/protoc-gen-validate),可以直接通过在PB文件字段中使用配置语言来描述字段的合法值,然后自动生成校验代码。也就是无需开发人员手动写代码来校验参数,而是直接自动生成,这个方案的优点在于可以很方便地写工具来度量和检查系统中哪些字段做了安全的校验,哪些字段没有做。官方的一个例子如下,大家可以感受一下有多方便。

syntax = "proto3";

package examplepb;

import "validate/validate.proto";

message Person {
  uint64 id    = 1 [(validate.rules).uint64.gt    = 999];

  string email = 2 [(validate.rules).string.email = true];

  string name  = 3 [(validate.rules).string = {
                      pattern:   "^[^[0-9]A-Za-z]+( [^[0-9]A-Za-z]+)*$",
                      max_bytes: 256,
                   }];

  Location home = 4 [(validate.rules).message.required = true];

  message Location {
    double lat = 1 [(validate.rules).double = { gte: -90,  lte: 90 }];
    double lng = 2 [(validate.rules).double = { gte: -180, lte: 180 }];
  }
}

通过扩展了PB的自定义能力,在定义字段后面通过添加形如“[(validate.rules).uint64.gt = 999];”的描述语言来规定该字段的字面值的合法范围,然后使用插件工具自行生成校验代码。不过需要注意,该项目目前处于alpha阶段,它们的API及实现随时都有可能被修改。