读软件开发安全之道:概念、设计与实施16安全开发最佳实践

1. 安全测试的最佳实践

1.1. 编写可靠的安全测试用例是提升任何代码库安全性的重要方式

1.2. “测试驱动的开发”(Test Driven Development,TDD)

  • 1.2.1. 在编写新代码的同时编写测试用例

1.3. 利用集成测试

  • 1.3.1. 集成测试(integration testing)可以把系统置于它们的目标环境中,确保通过单元测试的所有组件能够按照预期工作

1.4. 最明智的做法是把工作进行细分,每部分工作都有一个里程碑,各部分都可以把工作继续向前推进一步

1.5. 如果团队中来了一些新人需要学习代码,可以让他们编写安全测试用例

  • 1.5.1. 不仅是帮助他们学习代码的最好方法,而且可以为代码产生持久的价值

2. 代码质量

2.1. 如果我们可以提升代码的质量,就可以在很长时间内让代码更加安全,而无论我们能不能意识到这种安全性的提升

2.2. 所有漏洞都是错误(bug),所以错误越少意味着漏洞和漏洞链越少

2.3. 代码清洁

  • 2.3.1. 代码异味(code smell)、面条式代码(spaghetti code)和标记代码需要进一步完善的TODO 注释往往都是产生漏洞的沃土

  • 2.3.2. 使用工具来标记问题

  • 2.3.2.1. Lint和其他静态代码分析工具可以提供更丰富的代码审查,有时可以为我们解释代码的错误和漏洞

  • 2.3.2.2. 频繁使用相关工具来减少可能的错误数量

  • 2.3.3. 找个时间进行一些清理工作并不是一件简单的事情

  • 2.3.3.1. 我们可以用增量的方式,哪怕每周花上一两个小时,随着时间的推进,我们都可以给项目带来巨大的改善,这个过程也可以让我们更好地熟悉巨大的代码库

2.4. 异常和错误处理

  • 2.4.1. 认识到异常处理不佳的巨大风险,然后思考所有正确的响应方式,找到即使最不可能发生的意外情况

  • 2.4.2. 最好在尽可能接近源头的地方处理异常,因为越接近源头,与环境的关系就越密切,进一步产生复杂性的概率就越小

  • 2.4.3. 大型系统可能需要一个顶层处理器来处理所有突发的未处理异常事件

  • 2.4.4. 在错误处理中,异常处理不佳往往都会与潜在的漏洞关联

  • 2.4.5. 最好的方法就是从一开始就实施可靠的错误处理

  • 2.4.5.1. 在协同攻击中,引发错误可能正是攻击者的主观目的

  • 2.4.6. 为了能够正确地执行错误和异常处理,进行可靠的测试至关重要

  • 2.4.6.1. 确保所有代码路径都测试到位,尤其是那些不常用的代码路径

  • 2.4.7. 积极调查并且修复那些间歇发生的异常情况,因为如果聪明的攻击者知道如何触发异常情况,他们恐怕就可以对其进行微调,从而恶意利用其中的漏洞

2.5. 记录安全性

  • 2.5.1. 对于关键代码,或者那些我们需要对安全性加以解释的代码,注释格外重要,因为注释可以让那些考虑修改代码的人了解其中的利害关系

  • 2.5.2. 好的注释可以解释清楚问题所在

  • 2.5.3. 目的是对那些不够明显、在未来很容易被忽略的问题进行注释,以警告读者

  • 2.5.4. 注释永远无法替代那些知识渊博的程序人员,这类人员会对安全隐患时刻保持警惕,因为他们深知这些任务有多么艰巨

  • 2.5.5. 撰写一份好的安全测试用例是一种理想的文档备份方法,可以防止有人将来不知不觉地破坏代码的安全性

  • 2.5.5.1. 这种测试作为一种模拟攻击的方法,不仅可以防止有人不小心对代码进行了不利的变更,还可以准确地显示出代码中可能出现的错误

2.6. 安全代码审查

  • 2.6.1. 同级代码审查

  • 2.6.1.1. 涵盖了审查人员应该留意的潜在问题清单,包括代码是否正确、是否可读、代码的风格等

  • 2.6.1.2. 能够明确地包含安全性审查

  • 2.6.2. 考虑代码的安全性,也就是从安全性的角度重新审视这些代码,这一步可以在第一次阅读代码之后执行

  • 2.6.3. 重视安全性的代码都值得我们万分留意,我们对代码质量的要求也格外高

  • 2.6.4. 作为审查人员,如果我们认为某些输入可能存在问题,就应该撰写一份安全测试用例,看看会发生什么,而不是纯粹靠猜测

3. 依赖关系

3.1. 当今系统往往都建立在大量外部组件上,这种依赖关系在很多方面都是麻烦重重

3.2. 使用包含已知漏洞的旧版外部代码,是整个产业至今无法系统性解决的最大现实威胁之一

3.3. 在软件供应链中选择了恶意组件也是一大风险

3.4. 选择安全的组件

  • 3.4.1. 基本因素

  • 3.4.1.1. 相关组件及其厂商的安全追踪记录如何?

  • 3.4.1.2. 这个组件的接口是私有的吗?有没有其他能够兼容的产品?

>  3.4.1.2.1. 选择越多就越有可能有安全的产品可供选择
  • 3.4.1.3. 什么时候发现了组件中的安全漏洞?

  • 3.4.1.4. 我们对(产品的)厂商能够及时响应并提供修复方案有把握吗?

  • 3.4.1.5. 让这个组件时刻保持更新状态的操作成本(包括我们需要付出的努力、组件的宕机时间和我们需要支付的费用)有多高?

  • 3.4.2. 要让系统整体更加安全,这个系统的所有组件就都必须是安全的

  • 3.4.3. 组件之间的接口也必须是安全的

  • 3.4.4. 用来处理隐私数据的组件必须保证不会泄露信息

  • 3.4.4.1. 软件会记录数据的内容,或者把它保存在不安全的介质中,这就会增加数据泄露的风险

  • 3.4.5. 不要使用组件正式发布之前的原型(prototype)组件,其他不满足高质量产品要求的组件也要避免运用在系统当中

3.5. 保护接口

  • 3.5.1. 一个拥有良好记录的接口应该明确指出自己的安全和隐私属性

  • 3.5.2. 不要使用已经被弃用的API

  • 3.5.2.1. 弃用API的原因并不仅仅是安全性一项,但是作为API的调用者,我们一定要弄清楚API弃用的原因是不是存在安全隐患

  • 3.5.3. 那些包含复杂配置选项的API也需要我们格外警惕

  • 3.5.3.1. 遵守默认防御模式,把如何安全地配置系统记录下来,同时尽量提供辅助方法来确保配置的方法正确无误

  • 3.5.3.2. 如果我们必须暴露一些不安全的功能,就要尽可能确保没有人可以在不知道这些功能的作用时就无意中使用到这些功能

3.6. 不要做重复的工作

  • 3.6.1. 只要条件允许,就应该使用标准的、高质量的库来提供基本的安全功能

  • 3.6.2. 用一个库或者框架来解决一个潜在的安全问题往往就是最好的方法

  • 3.6.3. 安全漏洞可能非常微妙,攻击方式有很多种,攻击者只要成功一次就足够了

  • 3.6.4. 人工引入的错误永远都会带来发起攻击的良机,所以通过解决方案让人们把事做对,这才是最保险的方法

3.7. 对抗传统安全

  • 3.7.1. 安全技术的发展总是相对滞后一些

  • 3.7.2. 凡事皆有“保质期“

  • 3.7.3. 如果使用密码进行认证的方式很容易受到网络钓鱼攻击,那就采用双因素认证的方式

  • 3.7.4. 随着量子计算的不断成熟,高安全性的系统开始具备抵抗后量子时代算法攻击的能力

  • 3.7.5. 惯性本身就是一种强大的力量

  • 3.7.5.1. 系统往往都是用增量的方式渐渐演化的,所以没有人会质疑如今认证和授权的执行方式

  • 3.7.6. 企业安全架构往往都要求所有子系统能够相互兼容,所以所有变更都意味着每个组件要用全新的方式进行互操作

  • 3.7.7. 那些过时的组件也会带来各式各样的问题,因为传统软硬件可能无法支持当今的安全技术

  • 3.7.8. 没有什么简单的方法可以解决传统安全方法带来的隐患,但是威胁建模可以帮我们找出传统安全方法可能带来的问题,让这些技术引发的风险更加清晰可见

4. 漏洞分类

4.1. DREAD评估

  • 4.1.1. 最早由杰森·泰勒(Jason Taylor)构思

  • 4.1.2. DREAD评级是相当主观的

  • 4.1.3. 潜在破坏(Damage potential)

  • 4.1.3.1. 如果攻击者利用这个风险,它可以给我们造成多大的破坏?

  • 4.1.4. 成功率(Reproducibility)

  • 4.1.4.1. 攻击是每次都会成功,有时会成功,还是很少成功?

  • 4.1.5. 利用难度(Exploitability)

  • 4.1.5.1. 从技术难度上看,利用这个漏洞需要攻击者付出多少努力和资金?

  • 4.1.5.2. 攻击路径有多长?

  • 4.1.6. 影响的用户(Affected user)

  • 4.1.6.1. 攻击会影响所有用户、部分用户,还是一小部分用户?

  • 4.1.6.2. 攻击是针对特定目标,还是随机选择受害者?

  • 4.1.7. 发现难度(Discoverability)

  • 4.1.7.1. 攻击者发现这个漏洞的可能性有多大?

4.2. 大多数安全问题一旦被发现就不难修复,我们的团队都能迅速对修复的方法达成一致

4.3. 除非有重要限制条件规定人们必须采取权宜之计,否则只要有任何可以被加以利用的漏洞,我们都应该对这个漏洞进行修复

4.4. 几个小的代码错误结合在一起就会形成一个重大的漏洞,漏洞链由此产生

4.5. 编写利用漏洞的有效代码

  • 4.5.1. 用概念验证的方式构建一个有效的攻击,是解决漏洞问题的最强方法

  • 4.5.2. 对初学者来说,构建一个能够利用漏洞的示范代码需要完成大量的工作

  • 4.5.3. 实际利用漏洞的代码需要我们在发现底层漏洞之后进行大量的细化工作

  • 4.5.4. 即使是一位身经百战的渗透测试专家,也不能因为自己无法创建出利用漏洞的代码,就认定这个漏洞不可能被人利用

4.6. 做出分类决策

  • 4.6.1. 发现一个潜在的漏洞却置之不理,这样的错误只能用“可悲”来形容,这也正是我们应该着意避免的情况

  • 4.6.2. 趁早修复这样的缺陷才是长治久安之道

  • 4.6.3. 对于所有特权代码或者访问有价值资产的代码,其中的错误都应该修复,然后需要认真地进行测试,防止引入新的错误

  • 4.6.4. 那些被隔离在所有攻击面之外而且看起来无害的错误可以推迟修复

  • 4.6.5. 如果听到某个错误是无害的,一定要仔细加以确认:有时候,修复这个错误比评估它有可能给我们带来的影响更容易

  • 4.6.6. 尽快主动修复那些有可能属于某个漏洞链上的错误

  • 4.6.7. 如果是麻烦,我就建议你尽早解决

  • 4.6.7.1. 安全第一,没有后悔药可吃

  • 4.6.8. 不要浪费时间讨论假设的条件

  • 4.6.8.1. 应该把注意力集中在理解问题的各个方面

5. 维护一个安全的开发环境

5.1. 即便是开发过程中一次小小的失误,恶意代码也有可能趁机潜入产品,因此这时我们应该停下手里的工作

5.2. 开发和生产环境相分离

  • 5.2.1. 在开发软件的时候,开发人员不可以访问到生产环境中的数据

5.3. 保护开发环境

  • 5.3.1. 如果我们希望开发的成果安全无虞,那么参与开发的所有计算机都必须是安全的,同样所有源代码库和其他服务也必须是安全的,因为漏洞也有可能从这些地方潜入最终的产品

  • 5.3.2. 安全地进行配置、定期更新开发设备

  • 5.3.3. 限制开发设备的使用人员

  • 5.3.4. 系统地审查新的组件和依赖关系

  • 5.3.5. 安全地管理那些用来开发和发布产品的计算机

  • 5.3.6. 安全地管理密钥(包括代码签名密钥)

  • 5.3.7. 使用强大的认证机制,同时对登录证书进行妥善管理

  • 5.3.8. 定期审核异常活动的源变更方案

  • 5.3.9. 给源代码和源代码的开发环境保存一份安全的备份数据

5.4. 发布产品

  • 5.4.1. 使用正式的发布流程把开发和生产联系起来

  • 5.4.2. 建立一个共享的存储库,这个库只有开发人员可以修改,操作人员只有只读权限

  • 5.4.3. 权限分离不仅可以明确各方的责任,而且可以落实各方的责任

6. 及时采取行动

6.1. 错误越少就代表能够利用的漏洞越少

6.2. 称职的设计方案、尽职的代码编写、完整的软件测试和文档记录

7. 安全是每个人的职责

7.1. 安全分析最好由最理解这个软件的人来完成

7.2. 只有在安全成为整个软件生命周期中一个不可或缺的环节时,安全性才能得到最好的保障,但是长期花钱聘请安全顾问毕竟是不切实际的

7.3. 安全思维并不复杂,但是安全思维非常抽象

7.4. 所谓“广泛的安全参与”应该理解成整个团队一起努力,每个人都做自己最擅长的工作

7.5. 外部专家非常适合执行差距分析或者渗透测试这类任务,这样可以弥补组织机构自己的能力,用丰富的经验从全新的视角审视我们的产品

  • 7.5.1. 即使专家能够为整体安全实例做出重要贡献,一天时间到了之后他们也会立刻告辞

8. 避免亡羊补牢

8.1. 桥梁、道路、建筑、工厂、商店、水坝、港口、火箭等,都要在纸上进行精心设计和仔细审查确保安全之后,才能开工建造

8.2. 大多数软件都是先开发出来,然后才考虑保护措施的

8.3. 早期进行安全调查不仅可以帮我们节约时间,而且可以让我们获得可观的回报,提升产品的质量

8.4. 在最坏的情况下,把安全性问题放到设计阶段进行考虑的最具说服力的理由是避免因设计产生的安全漏洞

8.5. 好的安全设计决策可以让我们获益更多

  • 8.5.1. 安全设计可以在最大限度上减少在实施中引入漏洞的可能性,但是它并没有魔力,所以不可能让软件刀枪不入

8.6. 专注于软件安全性的设计方案审查是非常重要的一步,因为软件的功能审查采取的是完全不同的视角,提出的问题也从来不以安全为重

8.7. 不安全的设计方案也可以轻松通过这些测试,同时由此实现的软件也很容易遭到攻击的破坏

9. 未来安全

9.1. 我们每个人都可以做出贡献的方式是确保我们开发的软件质量可靠,因为这类软件都是在漏洞中汲取经验的产物

9.2. 随着我们的系统在功能和规模两个维度不断扩展,系统的复杂性也难免会随之增加

  • 9.2.1. 随着软件系统不断扩大,管理复杂性的难度也变得越来越高,这些系统也因此变得越来越脆弱

  • 9.2.2. 对一个大型信息系统,划分大量组件的对应安全模块是成功的必备要求

9.3. 提升安全性的一种方法是对传统的错误进行更好的分类,也就是考虑每项错误可以如何成为攻击链中的一个环节,然后对优先修复哪些错误进行排序

9.4. 从最低程度的透明性到最高程度的透明性

  • 9.4.1. 如果你无法测量,你就无法改进

9.5. 软件发行商基本都能在不影响未来安全的情况下提供更多的信息

9.6. 未来,负责任的软件发行商应该自行披露完整的信息,然后根据需要对披露的信息进行编辑,以免降低软件的安全性

9.7. 当代大型软件系统都是由大量组件构成的,这些组件都必须是由可靠实体从安全的子组件中使用安全工具集创建出来的,同时它们的来源也必须是真实的

  • 9.7.1. 我们都会使用一些必备工具来确保软件构建的完整性,而这些工具如今可以免费获得,我们也可以默认这些软件能够正常工作

9.8. 软件有一种独特的属性,它们完全是由比特(也就是一串0和1)组成的,所以我们完全可以凭空想象出一个软件

  • 9.8.1. 唯一的限制就是我们自己的想象力和创造力

  • 9.8.2. 在实现一个系统的过程中,我们应该保留所有关键决策和操作的记录以供审计之用,这样系统的安全属性才能落到实处

10. 移动设备上的数据安全

10.1. 数据保护的层级

  • 10.1.1. 对于移动设备来说,让用户(使用密码、指纹或者面部识别)解锁加密密钥才能访问受保护的数据,相当于每次打开金库都要去找银行经理

  • 10.1.2. 即使限制最低的保护级别——称为首次解锁后(After First Unlock,AFU),也存在严重的限制,这种保护级别会在启动之后要求用户提供证书来重构加密密钥

  • 10.1.3. 大多数App出于方便,采取了不对数据进行保护的做法,所以只要攻击者可以查看移动设备,所有数据也就成了刀俎上的鱼肉

10.2. 机密消息App是这类规则的一大例外,它们采用的是“完全保护”类

10.3. 云集成对很多App的重要性

  • 10.3.1. 为了利用云的强大能力,我们必须首先信任云,允许它处理我们的数据,而不能把加密后的数据锁在我们自己的本地设备上

  • 10.3.2. 所有这些无缝的数据访问都和强大的数据保护机制背道而驰,如果我们弄丢了连接云端的那台手机,安全性就更无从谈起了

  • 10.3.3. 依靠云的那些App基本上不会选择通过加密来保护数据

10.4. 移动设备处于一个庞杂的生态系统当中,除非数据保护机制可以作用于所有组件和场景,否则这些机制很难发挥它们的作用

10.5. 对于那些你很介意泄露出去的数据,不要把它们保存在你的手机里