This commit is contained in:
louzefeng
2024-07-09 18:38:56 +00:00
parent 8bafaef34d
commit bf99793fd0
6071 changed files with 1017944 additions and 0 deletions

View File

@@ -0,0 +1,179 @@
<audio id="audio" title="31 | 为什么安全的代码这么重要?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/37/cc/377f51c59670c018ff7cdf42868d83cc.mp3"></audio>
从今天开始,我们进入本专栏的“安全模块”。首先,我们通过一个具体的安全漏洞的案例,来感受下计算机代码是多么的脆弱,以及编写安全的代码为什么如此重要。
## 评审案例
在Web开发中“multipart/form-data“类型经常被用来上传文件。比如下面这段描述表单的代码就是使用multipart/form-data上传文件的一段HTML代码。
```
&lt;FORM action=&quot;http://upload.example.com/&quot;
enctype=&quot;multipart/form-data&quot;
method=&quot;post&quot;&gt;
&lt;P&gt;
Upload the file: &lt;INPUT type=&quot;file&quot; name=&quot;upload-file&quot;&gt;&lt;BR&gt;
&lt;INPUT type=&quot;submit&quot; value=&quot;Send&quot;&gt;
&lt;/FORM&gt;
```
文件上传的操作会被浏览器解析成类似下面的HTTP请求。
```
Content-Type: multipart/form-data; boundary=AaB03x
--AaB03x
Content-Disposition: form-data; name=&quot;upload-file&quot;; filename=&quot;myfile.txt&quot;
Content-Type: text/plain
... contents of myfile.txt ...
--AaB03x--
```
Web服务器接收后会解析这段请求然后执行相关的操作。下面的这段代码是2017年3月之前Apache Struts 2解析“multipart”请求的实现。
<img src="https://static001.geekbang.org/resource/image/9a/60/9a01e8ba62aa08e35d116d93e1e42e60.jpg" alt=""><br>
其中蓝色标注的代码LocalizedTextUtil.findText()用来查找错误的本地化信息。如果“multipart”请求解析出错就会触发这个方法。它的规范大致如下
<img src="https://static001.geekbang.org/resource/image/7e/73/7ecef79094e02086610f20dcc5be0773.jpg" alt=""><br>
对于LocalizedTextUtil.findText()的规范我们要留意蓝色字体的部分。这一部分告诉我们如果信息里包含了OGNLObject Graph Navigation Language的表达式表达式会被执行。
我们把上面的信息放到一块儿来看看如果“multipart”请求解析出错会调用LocalizedTextUtil.findText()来查找本地化的错误信息如果错误包含OGNL表达式表达式会被执行以获取解释后的信息本地化的错误信息会返回给请求者比如浏览器
能不能构造一个包含OGNL表达式的“multipart”请求对于熟悉HTTP协议和OGNL表达式的用户来说这是一件轻而易举的事情。如果“multipart”请求不合法OGNL表达式会被执行执行的结果以错误信息的形式返回给请求者。
通过“巧妙地”设计OGNL表达式攻击者可以定制执行的指令从而定制返回错误信息的内容。这样攻击者几乎可以获得任何他想要的有价值的内部信息。这就是一个由代码引起的安全漏洞。这个安全漏洞的[危险等级是10.0分](https://www.first.org/cvss/calculator/3.0#CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H)(请参见下一节“如何评估代码的安全缺陷”),是一个危险等级最高的漏洞。
我们回头看FileUploadInterceptor.intercept()的这段实现代码时,它的危险性其实很清楚,主要有两点:
<li>
没有充分了解调用接口LocalizedTextUtil.findText()
</li>
<li>
允许执行远程请求的表达式OGNL表达式
</li>
这两点分别违反了下面的安全编码原则:
<li>
清楚调用接口的行为;
</li>
<li>
跨界的数据不可信任。
</li>
## 真正的威胁
我们一起来看看这个漏洞的几个关键时间点:
<li>
2017年1月29日NIST的NVDNational Vulnerability Database )接收到了这个漏洞报告。
</li>
<li>
2017年3月6日GitHub上出现了漏洞的描述和攻击示例。
</li>
<li>
2017年3月7日Apache Struts发布了这个漏洞的修复版本Struts 2.3.32和2.5.10.1。
</li>
<li>
2017年3月7日以及随后的几天出现了更多的攻击示例很多媒体和专家开始分析这个漏洞推荐可能的漏洞防范措施提醒升级Apache Struts到安全的版本。
</li>
我们要特别留意两段时间第一段时间是1月29日到3月7日。这一段时间安全漏洞已经被发现但是并没有被公开。这说明这个安全研究者极有专业素养。我猜想这位名字叫“Nike Zheng”的研究者在2017年1月29日之前把他的研究成果通知了Apache Struts。然后双方共同努力将这个漏洞一直保密到2017年3月7日。这一段时间的保密工作非常重要要不然漏洞修复之前会有大批的应用暴露在黑客的攻击之下。
寻找并且通知受到安全漏洞影响的软件供应商,然后双方共同保密一段时间,给漏洞修复留出足够的时间,这是安全研究者的通常做法。**如果你认真学习了本专栏的“安全”模块,发现现存代码的安全问题,并且构造出可行的攻击方案,并不是一件特别困难的事情。如果以后你通过阅读代码,发现了一个漏洞,公布漏洞之前,请务必联系代码的维护者,做好漏洞的保密工作,并给他们预留充足的修复时间。**
第二段时间是2017年3月7日这一天漏洞的修复版本发布漏洞的补丁公之于众漏洞的细节也就随之公开。专业的研究者和黑客会迅速地解剖漏洞研究攻击方式。留给应用系统的时间并不多一定要想方设法在最短的时间内升级到修复版本。做到这一点并不容易。**大部分有效的安全攻击,都是发生在漏洞公布之后,修复版本升级之前。这一段时间,是最危险的一段时间。**
## Equifax的教训
2017年9月7日美国最大的征信公司Equifax宣称7月29日公司发现遭遇黑客攻击该攻击始于5月中旬大约有1.45亿条信用记录被盗取其中包括20多万用户的支付卡信息。在美国包括社会保障号、出生日期在内的信用记录是高度敏感的信息。有了这些信用记录一个人不用出面甚至不需要支付一分钱就可以买车、买房、申请信用卡。
果然有人报告自己被冒名顶替买了车、买了宠物。对于一个依靠安全生存的公司这种情况的发生无疑是令人沮丧的。随后的几天时间里Equifax的股票下跌超过了30%蒸发了折合大概60亿美元的市值。
是什么样的安全漏洞导致了这么大的损失Equifax公司后来确认引起黑客攻击的漏洞最主要的就是我们上面讨论过的Apache Struts漏洞。
Apache Struts于2017年3月7日发布了针对该漏洞的修复版本。但是Equifax一直到7月底都没有完成安全版本的升级将自己敞露在风险之下。
从3月漏洞细节公布到5月中旬黑客用了两个月的时间设计了攻击方案然后从5月中旬到7月底又用了两个多月的时间从容地获取了数亿条信用记录。
如果按照严重程度来算这一次黑客攻击可以排进21世纪已知的重大信息安全事故的前三名。而且这次安全事故的影响范围远远超出Equifax公司本身。
人们对征信公司的信任,降低到了前所未有的程度,纷纷冻结自己的征信记录,不允许任何人查询;银行的信用部门,必须更加谨慎地防范信用欺诈,要投入更多的财力、人力。所有受到影响的用户,必须采取更加严格的措施保护自己在其他征信机构、金融机构、保险机构的信用状态。
所有的这些问题归根到底都是因为没有及时地完成安全修复版本的升级。这里面固然有技术的问题但更多的是管理的问题。2017年9月15日Equifax的首席信息官和首席安全官宣布退休。
五行不起眼的代码,酿造了一起损失数十亿美元的安全事故。受到影响的人群,也可能包括这个漏洞的研究者和修复者,系统的运营者,甚至是攻击者本人。这种不对称的破坏性让人唏嘘,这也正是我们为什么要重视代码安全的背后的原因。
Equifax的教训给我们带来三点启示
<li>
**不起眼的代码问题,也可以造成巨大的破坏**
</li>
<li>
**安全修复版本,一定要第一时间更新**
</li>
<li>
**安全漏洞的破坏性,我们很难预料,每个人都可能是安全漏洞的受害者**
</li>
## 编写安全的代码
一般来说,安全的代码是能够抵御安全漏洞威胁的代码。
传统上我们说到信息安全的时候最常接触的概念是防火墙、防病毒、防攻击。其实大部分的安全事故80%-90%)是由软件的代码漏洞引起的。没有安全保障的代码,是随时都可以坍塌的空中楼阁。
## 小结
通过对这个案例的讨论,我想和你分享下面三点个人看法:
<li>
**不起眼的小问题,也会有巨大的安全缺陷,造成难以估量的损失**
</li>
<li>
**编写安全的代码,是我们必须要掌握的基础技能**
</li>
<li>
**安全问题,既是技术问题,也是管理问题**
</li>
下一节,我们接着聊安全漏洞的威胁该怎么衡量。再接着,我们来讨论一些常见的编写安全代码的原则和实践。
## 一起来动手
Equifax公司的问题之一就是没有及时地更新安全修复。这一般不是疏漏的问题而是没有充分认识到安全更新的重要性或者没有把安全修复的计划执行到位。
要想升级到安全修复的版本,我们需要知道两件事:
<li>
第一时间获知,某个依赖的软件有了安全更新;
</li>
<li>
最快速地行动,升级到安全修复版本。
</li>
有时候,安全版本升级之前,安全漏洞的细节就已经暴露出来了。这时候,我们也要采取必要的措施:
<li>
第一时间知道出现了安全漏洞;
</li>
<li>
快速寻找、部署漏洞修复的临时方案。
</li>
人力总是有限的,我们接触到的信息也是非常有限的。上面的两种措施中,人工都没有办法做到第一点的,除非你使用的是一个完全封闭的系统(完全封闭的系统,一般也是漏洞更多的系统);而第二点,或多或少的,都需要人工的参与。
我们利用讨论区,来讨论三个问题:
第一个问题是,你有没有使用最新版本软件的习惯?
第二个问题是,你的公司是如何获取安全漏洞信息和安全更新信息的?
第三个问题是,你的公司有没有安全更新的策略?如果有,又是怎么执行的,能不能执行到位?
欢迎你在留言区留言、讨论,我们一起来学习、思考这些老大难的问题!
如果你觉得这篇文章有所帮助,欢迎点击“请朋友读”,把它分享给你的朋友或者同事。

View File

@@ -0,0 +1,274 @@
<audio id="audio" title="32 | 如何评估代码的安全缺陷?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/a9/c5/a9dc03279420e27f805849f04d2341c5.mp3"></audio>
我自己有一点小小的强迫症,遇到事情,喜欢自己动手整个清楚明白。我的大部分失眠,都要拜这点强迫症所赐。时间永远都不够用。如果上天给我一个机会,我是不是可以借上五百年?其实,借上五百年,时间一定还是不够用的。
我经常被问到三个问题:
<li>
有什么事情是你必须要做的?
</li>
<li>
哪些事情是只有你能做的?
</li>
<li>
哪些事情是别人可以帮你做的?
</li>
这就是一种时间管理的思路,隐含的意思是:
<li>
识别并且选择最重要的事情;
</li>
<li>
确定自己最擅长的事情,全力以赴地做好;
</li>
<li>
选择你的帮手,充分信任并授权。
</li>
评估软件的缺陷就是这个思路运用得最广泛的一个场景。作为程序员,我们需要了解软件存在的问题,以及问题的严重程度。那么,我们该如何评估软件存在的问题,以及代码的安全问题呢?
## 关注用户感受
软件缺陷的定义方式和衡量方式有很多种。**从用户感受的角度出发,定义和计量软件缺陷**,是其中一个比较好的、常用的软件缺陷评估体系。我个人比较倾向一种观点,**软件缺陷的严重程度应该和用户的痛苦程度成正比**。
从用户感受出发,衡量软件缺陷有两个最常用的指标:
<li>
**缺陷影响的深度**,软件缺陷带来的问题的严重性:软件缺陷导致的最严重的问题是什么?
</li>
<li>
**缺陷影响的广度**,软件缺陷带来问题的可能性:软件缺陷导致问题出现的频率是多大?
</li>
比如说一个外卖系统只要订餐金额超过32766元就无法提交订单因为软件系统允许的最大金额为32766元。这种事情 一旦发生就是一个非常严重的错误订单无法提交影响的深度。但是在外卖系统里金额超过3万元的订单数量应该不多该错误发生的几率并不大影响的广度
如果我们把每个指标都划分高、低两种程度,就能得到如下四种情况:
<li>
高严重性,高可能性;
</li>
<li>
高严重性,低可能性;
</li>
<li>
低严重性,高可能性;
</li>
<li>
低严重性,低可能性。
</li>
依据这四种情况,我们就可以定义软件缺陷的优先等级了:
<li>
高优先级 P1 高严重性、高可能性;
</li>
<li>
中优先级 P2 高严重性、低可能性; 低严重性、高可能性;
</li>
<li>
低优先级 P3 低严重性、低可能性。
</li>
<img src="https://static001.geekbang.org/resource/image/35/b4/355170caae4a69136f6c4974c03359b4.png" alt=""><br>
上述外卖系统的软件缺陷的优先级应该是中等优先级P2高严重性、低可能性
## 缺陷,需要从外向内看
假设这个外卖系统有一个bug订餐金额可以是0或者负数。如果订餐金额是负数餐厅不仅需要送餐上门还需要倒贴钱。这可是一个好玩的bug当然不能坐视不管。
过一段时间bug修好了。大部分餐厅都能接受新系统。可是有一个商家表达了不同意见。原来他们有一个“寻找美食家”的活动餐厅不仅不要顾客的钱还倒贴同等餐费请客人品尝最新餐品。负数金额正是他们倒贴钱的“定价”。 美其名曰“你尝,我负”。该商家声称,新系统存在一个严重的缺陷,定价无法使用负数,导致这个活动无法进行。
如果你是这个外卖系统的工程师这个bug你修不修
你是否认可和接受这个缺陷报告背后反映的就是你看待这个软件缺陷的态度。如果从用户感受的角度出发定义和计量软件缺陷这就是一个符合条件的bug。这个问题对该商家的影响非常大无法开展正常的商业活动。但这个问题发生的可能性比较小大概只有这么一个商家使用负数定价。那么这个软件缺陷的优先级是中等优先级P2高严重性、低可能性
你看, 这个外卖系统的代码变更本身并不存在真正的缺陷,但是,如果从用户角度来看,又的确存在一个中等优先级的缺陷。我们当然可以认为,这个缺陷应该在用户那里得到校正。但是有时候,用户并没有校正这种缺陷的机会和能力。
你会不会觉得这个例子有点离奇、离谱,甚至有点搞笑?其实处理这种事情,是我日常工作中非常重要的一部分。 一旦Java的接口规范和规范实现发布我们并不知道在现实世界中用户如何运用他们的聪明才智发挥他们的创造性灵活地使用这些接口和实现。而无论用户怎么使用在软件升级变更中我们都没有充分的理由打断用户的应用运转和商业运营。所以软件的升级或者变更处处充满了乐趣和挑战步步惊心。
**在一个好的软件缺陷评估体系中,不是只有代码错误才会被关注,没有错误的代码,也可能存在需要变更或者改进的“缺陷”**。这就是我们要强调的,从用户的感受出发,定义和计量软件缺陷。缺陷,需要从外向内看。
很多时候,程序员认为是严重的缺陷,用户可能一点儿都感受不到;程序员认为无关紧要的事情,放到了用户的使用场景中,可能就是非常严重的事故。从外向内看缺陷,要求我们站在用户的角度思考问题,看待缺陷。这是一个可以让我们深切关注用户感受的视角。**从用户视角出发的决策,可以让我们的时间使用得更有市场价值。**
## 细化的优先级
在一定程度上,作为软件的原作者或者维护者,我们被各种各样的软件缺陷包围着,永远存在修补不完的缺陷,永远存在无法修复的问题。上述软件缺陷的优先等级的定义,稍显粗糙。我们可能需要更细致一些的等级划分,以便更好地安排我们的时间和区分做事情的轻重缓急。
如果我们把每个指标都划分高、中、低三种程度,就可以得到九种情况,定义五种优先等级。五种等级,是一个常用的优先级数目。太少了,显得粗糙;太多了,容易迷糊。
<li>
第一优先级 P1 高严重性、高可能性;
</li>
<li>
第二优先级 P2 高严重性、中可能性;中严重性、高可能性;
</li>
<li>
第三优先级 P3 高严重性、低可能性;中严重性、中可能性;低严重性、高可能性;
</li>
<li>
第四优先级 P4 中严重性、低可能性;低严重性、中可能性;
</li>
<li>
第五优先级 P5 低严重性、低可能性。
</li>
<img src="https://static001.geekbang.org/resource/image/80/24/80fd5d616f4d315565d9a0156bf33324.png" alt=""><br>
你自己试试看,上面我们讨论过的外卖系统的软件缺陷属于第几优先级?
## 优先级的灵活性
**软件缺陷优先等级的定义是为了帮助我们更好地解用户的感受程度,以及安排时间和处理事情。**
由于时间和资源有限,在大多数情况下,特别是对于职业的程序员来说,并不能在一定时间内修复所有的缺陷,满足所有的变更要求。
实际工作中,我们有时候需要调节软件缺陷的优先等级,比如说:
<li>
如果已经存在应对办法,优先等级可以下调;
</li>
<li>
如果软件缺陷引起广泛的公开关注,优先等级应该上调;
</li>
<li>
如果软件缺陷影响了重要的客户,优先等级应该上调;
</li>
<li>
如果软件缺陷影响了重要的商业运营,优先等级应该上调。
</li>
对于一般的软件缺陷管理五个等级是一个恰当的优先级分割。然而除非特别注明仅从优先级别来看我们并不清楚P3缺陷的严重性是高是低或者发生的可能性是高是低而且问题的严重性在哪儿体现可能性又是如何度量的 这些也都是模糊的地方,可能受主观影响比较大。但是有一些软件缺陷,需要对这些问题有一个更加清晰的认识和感受,比如软件安全漏洞。应该如何评估软件的安全漏洞? 我们会在稍后的接着聊这个话题。
## 管理好自己的时间
好了,我们定义了软件缺陷的优先级,是时候看看如何使用它管理我们的时间了。还记得开头提到的三个问题吗?
1.有什么事情是你必须要做的?
P1的事情需要我们立即全力以赴、必须完成P2的事情需要我们协调资源尽快完成P3的事情需要我们密切关注尽量完成。
2.哪些事情是只有你能做的?
只有你能够修复的bug你可以记到自己名下负责修复这些缺陷。
3.哪些事情是别人可以帮你做的?
适合别人修复的bug如果还没有记到别人名下你可以琢磨下谁是最合适的人选然后和他商量看他有没有时间愿不愿意负责这个缺陷。当然别人也可能会问你愿不愿意修复另外一些缺陷。
相信我,大部分情况下,在得到足够的尊重以及有适当时间的前提下,人们愿意做些有意义的事情。
如果P1、P2、P3的问题修复完了你就可以放心休假去了。休完假充分从疲劳中恢复过来后你就可以考虑是不是可以看看P4和P5的问题了。
## 安全漏洞,需要大范围协作
在软件缺陷中,安全漏洞是一个奇异的存在。软件的安全漏洞,常常会导致非常严重的后果,以及恶劣的影响,甚至会直接导致一个公司的破产。
**由于编写安全代码本身的挑战性,以及消除安全漏洞的复杂性,业界通常需要进行大范围的合作,以便准确、快速、周全地解决安全缺陷问题。**大规模协作需要标准的描述语言,以及对安全问题的准确认知。[通用缺陷评分系统CVSS](https://www.first.org/cvss/)就是一种评判安全缺陷优先等级的标准。
对于安全缺陷,我们还可以使用上面提到过的严重性和可能性两种指标进行衡量。对这两种指标进行细化,才能更符合安全缺陷的特点。
对于安全缺陷的严重性,有四个互相独立的测量维度(量度):
<li>
对私密性的影响Confidentiality
</li>
<li>
对完整性的影响Integrity
</li>
<li>
对可用性的影响Availability
</li>
<li>
对授权范围的影响Authorization Scope
</li>
>
<p>题外话:<br>
私密性、完整性以及可用性,是描述信息安全的最基本的三个元素。<br>
私密性指的是数据未经授权,不得访问,解决的是“谁能看”的问题。<br>
完整性指的是数据未经授权,不得更改,解决的是“谁能改”的问题。<br>
可用性值得是数据经过授权,可以访问,解决的是“可以用”的问题。</p>
对于安全缺陷的可能性,有四个互相独立的测量维度(量度):
<li>
安全攻击的路径Attack Vector
</li>
<li>
安全攻击的复杂度Attack Complexity
</li>
<li>
安全攻击需要的授权Privileges Required
</li>
<li>
安全攻击是否需要用户参与User Interaction
</li>
由于这些测量维度都是相互独立的,二维的平面图已经不足以表示这么多维度了。通用缺陷评分系统使用了**标识符系统**和**计分系统**,通过标识符来标识测量维度的指标,通过十分制的计分来衡量安全问题的严重程度。由于测量维度的增多以及评分计算的复杂性,我们通常使用工具来记录和查看安全缺陷问题的等级。
比如,本文评审案例的那个缺陷,并不是一个安全问题。 如果我非要使用通用缺陷评分系统来描述它这个计分应该是0.0分,[直观描述](https://www.first.org/cvss/calculator/3.0#CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:N)看起来如下图:
<img src="https://static001.geekbang.org/resource/image/e9/bd/e9476045a739a8b96f67fdb163d60cbd.png" alt=""><br>
我们曾经谈到过"[goto fail](https://time.geekbang.org/column/article/77048)"这个安全问题如果使用通用缺陷评分系统计分是7.2[直观描述](https://www.first.org/cvss/calculator/3.0#CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:N)如下图所示:
<img src="https://static001.geekbang.org/resource/image/c6/90/c6a735042e72364a2ad1689c79869690.png" alt=""><br>
今天我给你介绍了通用缺陷评分系统的一些最基本的概念,先帮你形成一个基本的印象,算是一块敲门砖。你可以进一步了解[通用缺陷评分系统](https://www.first.org/cvss/)的有关[规范](https://www.first.org/cvss/specification-document)和[工具](https://www.first.org/cvss/calculator/3.0)。
## 安全漏洞和软件缺陷优先级
为了方便管理,安全漏洞和软件缺陷通常使用同一个代码缺陷管理系统。我们要注意两点:第一点是安全漏洞细节不可泄露;第二点是和普通软件缺陷相比,安全漏洞要优先考虑。
### 安全漏洞细节不可泄漏
我们反复强调过,软件的安全漏洞常常会导致非常严重的后果,以及恶劣的影响。最糟糕的是,我们并不能总是预料到谁可以利用这些漏洞,以及由此带来的后果有多严重。所以处理安全漏洞的态度,一定要保守。
安全漏洞不能像普通的代码缺陷那样,可以公开细节、公开讨论。相反,安全漏洞的知情人员一定要控制在一个尽可能小的范围内,知道的人越少越好。如果安全漏洞和普通缺陷共享一个代码缺陷管理系统,一定要想办法做好安全漏洞信息的权限管理。
### 安全漏洞要优先修复
一旦发现一个安全漏洞,不管是来源于外部情报,还是内部发现,我们都要考虑最快地修复,不要等待,更不要拖沓。即使我们全力以赴地修复,在系统修复之前,安全攻击随时都有可能发生。我们能做的,就是尽最大努力,缩短这段时间。
所以大部分的安全漏洞问题都是属于P1级别的缺陷。有一小部分深度防御的安全问题优先级可以是P2。安全问题不要使用P3及以下优先级。另外在所有的同级缺陷中安全问题要优先考虑。
## 小结
今天,我们讨论了如何评估软件存在的问题。软件的缺陷问题,要考虑缺陷影响的深度(严重性)和广度(可能性)。为了更好地认识安全漏洞,我们还要了解安全缺陷的评价标准。
有了软件缺陷的优先级,我们就可以更好地管理我们的工作和时间。下面的三个问题可以帮助你做好安排:
<li>
有什么事情是你必须要做的?
</li>
<li>
哪些事情是只有你能做的?
</li>
<li>
哪些事情是别人可以帮你做的?
</li>
## 一起来动手
我们要想掌握安全编码的技术,熟练修复软件漏洞的实践,需要先过三道关。
第一道关是意识Conscious。也就是说要意识到安全问题的重要性以及意识到有哪些潜在的安全威胁。
第二道关是知晓Awareness。要知道软件有没有安全问题安全问题有多严重。
第三道关是看到Visible。要了解是什么样的问题导致了安全漏洞该怎么修复安全漏洞。
比如我们上一次谈到的Equifax公司的信用记录泄漏的安全问题首席信息官和首席安全官之所以退休我们从外部可以猜测到的原因就是意识不够强烈没有及时更新安全修复。之所以没有及时地更新大概率是不知道有安全问题或者不知道安全问题有多严重。
系统管理员,最少需要过两关(意识和知晓);而软件开发工程师,需要过三关(意识、知晓和看到)。这三关并不容易过。
记得我们前面讨论的“安全漏洞细节不可泄漏”的实践吗?知道安全漏洞的人越少越好。这样保守的安全防范实践,和培养优秀的软件工程师所要求的实践,就成了悖论。
掌握编码安全技术,要培养意识,要知道得更多,要看到问题的细节。而“安全漏洞细节不可泄漏”的实践,却要求尽量不要公开安全细节。但是看不到细节的时候,我们就很难掌握这些技术,很难认识到威胁的严重性,从而阻碍了安全意识的培养。面对这一对几乎不可调和的矛盾,我们该怎么办?!
有数据披露2019和2020两年全球信息安全专业人员的缺口会有300多万。资源不足需求强劲我们怎样才可以学好安全代码的编写技能提升自己的价值
欢迎你在留言区留言、讨论,分享你的经验。我们一起来学习、思考这些老难老难的现实问题!
如果你觉得这篇文章有所帮助,欢迎点击“请朋友读”,把它分享给你的朋友或者同事。

View File

@@ -0,0 +1,304 @@
<audio id="audio" title="33 | 整数的运算有哪些安全威胁?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/aa/83/aa7059bd7358f5a7d55aa774fac14583.mp3"></audio>
在我的日常工作中,有一类错误,无论是原理还是后果,我都十分清楚。但是写代码的时候,这类错误曾经还是会反复出现。如果不是代码评审和代码分析环节的校正,我都很难意识到自己的代码中存在这样的缺陷。今天,我想和你聊聊,那些“**道理我都懂,但代码就是写不好**”的老顽固问题。
你不妨先停下来想一想,你有没有类似的经历? 又是怎么解决的呢?
## 评审案例
HTTP连接经常被中断或者取消如果客户端已经获得一部分数据再次连接时应该可以请求获取剩余的数据而不是再次请求获取所有数据。这个特性背后的支持协议就是HTTP范围请求协议RFC 7233
比如下面的例子客户端请求服务器返回image.jpg图像的前1024个字节。其中“bytes=0-1023”表示请求传输的数据范围是从0到第1023位的字节0-1023以及“-512”表示请求传输数据的最后512个字节-512
```
GET /image.jpg HTTP/1.1
Host: www.example.come
Range: bytes=0-1023,-512
```
如果服务器支持该协议,就会只传输图像的指定数据段。响应消息的代码大致如下所示:
```
HTTP/1.1 206 Partial Content
Content-Type: multipart/byteranges; boundary=THIS_STRING_SEPARATES
Content-Length: 2345
--THIS_STRING_SEPARATES
Content-Type: image/jpeg
Content-Range: bytes 0-1023/2048
...
(binary content)
--THIS_STRING_SEPARATES
Content-Type: image/jpeg
Content-Range: bytes 1535-2047/2048
...
(binary content)
--THIS_STRING_SEPARATES--
```
如果服务器端使用下属的代码验证请求数据的指定数据段C语言你来看看可能存在什么严重的问题
```
/**
* Check if the requested range is valid.
*
* start: the first byte position of the range request
* end: the end byte position of the range request
* contentLength: the content length of the requested data
* sum: the sum of bytes of the range request
*/
bool isValid(int start, int end, int contentLength, int* sum )) {
if (start &lt; end) {
*sum += end - start;
if (*sum &gt; contentLength) {
return false;
} else {
return true;
}
} else {
return false;
}
}
```
## 案例分析
上面的代码简化自Nginx HTTP服务器关于HTTP范围请求协议在2017年6月份的实现版本。由于讨论的需要我较大幅度地删减和修改了原来的代码。如果有兴趣你可以阅读并比较[2017年7月份的实现版本](http://hg.nginx.org/nginx/file/e3723f2a11b7/src/http/modules/ngx_http_range_filter_module.c)和[2017年6月份的实现版本](http://hg.nginx.org/nginx/file/aeaac3ccee4f/src/http/modules/ngx_http_range_filter_module.c),看看都有哪些比较有意思的改动。
上面的代码有许多问题,其中有一个致命的魔鬼藏在下面的一行代码中:
```
*sum += end - start;
```
我们假设数据长度为1024个字节HTTP请求的范围有两段第一段的请求字节数是一个正常的数据 比如1023而第二段的请求字节数的是一个巨大的数据比如INT_MAX - 23以至于两段数据相加时发生了整数溢出。本来应该是一个很大的数可是这个数超出了整数可以表达的范围结果就发生整数溢出变成了一个很小的数 比如1023 + INT_MAX - 23结果是999或者-2147482649。这样就通过了上述的HTTP请求范围验证。
第一段数据也许可以正常使用。然而,由于实际的数据长度不足以满足第二段数据范围请求,就有可能出现非常复杂的状况。 比如说为了加快反应速度提高服务器效率很多HTTP服务器都使用了缓存技术Nginx也不例外。
如果使用缓存技术,数据可能并不是直接从原始数据源读取,而是读取缓存的数据,而缓存的数据是一个临时的大集合,可能包括各种各样的数据,包括敏感数据。
如果发生读取范围溢出,目标数据段之外的缓存数据可能被读取。而目标数据段之外的数据,可能并没有授权给这个用户使用,这样就可能发生敏感信息的泄露。甚至通过设计,攻击者也有可能更改目标数据以外的非授权数据。这样就间接地操纵了服务器。
关于这个安全漏洞的更多描述,请参阅[CVE-2017-7529](https://nvd.nist.gov/vuln/detail/CVE-2017-7529)。这是一个[通用缺陷评分系统](https://www.first.org/cvss/)评分为[7.5](https://www.first.org/cvss/calculator/3.0#CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N)的严重安全缺陷。由于Nginx的广泛部署与使用该漏洞影响了一大批安装了Nginx服务器的系统。
出问题的表达式使用太普遍了,谁能想到会出现这么严重的安全漏洞呢。我们感兴趣的问题是:**整数的加法,如此普遍的运算,如果都这么脆弱,到底该怎么办才好?**
## 整数的陷阱
一个优秀的语言,一定是便于学习和使用的语言。语言语法的设计通常会考虑我们的现有习惯,降低使用门槛。比如,加法运算通常会设计成我们最熟悉的样子:
```
a = b + c;
```
我们如此熟悉这种表达式,一旦需要加法运算,这种表达式一定首先从我们的脑袋里自动跳出来。我们毫无察觉,也不会有意识地去追究这样一个从学习算数起就开始使用的表达式到底有什么问题。
那么到底有什么问题呢?
第一个问题是我们使用上述表达式时每个数字都可以是无限大的然而在计算机的世界里数字大小都是有限制的。比如整数一般使用32位bit或者4个字节byte来表示整数的大小不能超过32位能够容纳的范围。
### 整数运算,可能溢出
如果我们使用一位bit来表达一个整数那么通常就会是下面这种情况
```
0 + 0 = 0
0 + 1 = 1
1 + 1 = 0
```
注意1 + 1的结果不是我们习以为常的2而是0。这是为什么呢 因为2太大了一位的空间不够用的所以就“溢出”了。“溢出”导致这个运算的结果是0而不是预想的2。
再比如说我们表示时间的时候如果采用12小时制12点过一个小时就是1点而不是13点如果采取24小时制24点过两个小时就是2点钟而不是26点。
如果我们对于一位的数以及24小时制还算“清醒”的话那么对于32位或者64位的数可能就没那么重视了。你的代码里有没有涉及到整数的运算有没有潜在的溢出问题 我是经常会掉入这个陷阱的。
整数溢出的问题曾经在1995年导致[火箭的坠落](https://around.com/ariane.html) 在2016年导致[错误地签发了四千多万美元最高限额原为1万美元的博彩奖券](https://arstechnica.com/tech-policy/2017/06/sorry-maam-you-didnt-win-43m-there-was-a-slot-machine-malfunction/)。还有绵延不绝的,你我知道抑或不知道的软件安全漏洞。
我们更关心的是,该怎么避免这类错误?
**首先最重要的,是要借助软件开发的机制,减少代码错误**。比如我们在专栏开始讲的[借助重重关卡](https://time.geekbang.org/column/article/76349)减少错误。虽然我在编写代码的时候会时常忘却这个问题,但在评审代码时,有时候还能够记住这个问题的危害。多一双眼睛,就多了一处关卡。
然后,还要了解一些小技巧,我们看看都有哪些?
1.**比较运算,选择“比较小的数”**。
如果表达式出现在比较运算符的两侧,选择产生较小的数的运算。比如下面这段代码:
```
// a, b, c are positive integers
if (a &lt; (b + c)) { // (b + c) can overflow
// snipped
}
// a, b, c are positive integers
if ((a - b) &lt; c) { // no overflow
// snipped
}
```
这个例子适用于正数的运算,如果是负数呢? 如果不确定是整数还是负数,该怎么办?这个问题,我留给你去思考。
2.**限定数的范围,选择冗余的空间**。
如果现实需要32位的整数就选择64位的存储空间也就是说使用64位的整数类型进行运算。如果现实需要31位的整数就选择32位的存储空间。限定了数的范围一定要记得检查数据的范围千万不可超越这个范围。
```
private static int MAX_DATA_SIZE = 16384; // 2^14 bytes at mosts
private final ByteBuffer cache =
ByteBuffer.allocate)(MAX_DATA_SIZE); // limit the capacity
static int receive(byte[] data) {
if (data == null || data.length == 0) { // input check
// snipped
} else if (data.length &gt; MAX_DATA_SIZE) { // check the range
// throw exception, snipped
} else {
if (data.length &gt; cache.remaining()) { // check the add-up
// throw exception, insufficient space, too much data
}
if ((data.length + 1024) &gt; cache.remaining()) {
// safe '+', as the numbers are limited to 2^14.
// snipped
}
// snipped
}
// snipped
}
```
上面例子中的加法运算就是安全的。因为运算涉及的数据都被限定在14位范围内而两个数相加最多不超过15位。由于我们使用了32位的整数作为数据的类型那么15位的数据就不会产生溢出问题。
但是,这种方法要求我们时刻绷紧神经,仔细地定义、检查每个数据的限定范围,对我们自身要求相对有点高。所以涉及到比较运算,我还是建议使用“比较小的数”的办法。
```
- if ((data.length + 1024) &gt; cache.remaining()) {
+ if ((data.length - cache.remaining()) &gt; 1024) {
```
3.**检查数据溢出**。
检查数据溢出,虽然代码看起来有点多,但这总是一个有效可行的办法。比如,评审案例中的缺陷修复,就采取了类似如下的修改:
```
+ if (*sum &gt; (NGX_MAX_OFF_T_VALUE - (end - start))) {
+ return false;
+ }
*sum += end - start;
```
从Java 8开始Java提供了数据溢出保护的运算方法比如Math.addExact(int, int)执行两个整数相加的运算如果有整数溢出就会抛出ArithmeticException的异常。这些方法也许并不如直接使用运算符直观但是它们提供了额外的保护机制。如果我们不能确定溢出是否会发生使用这些方法可以让我们的代码获得更加深度的保护。
```
int sum = Math.addExact(a, b); // sum = a + b
```
整数溢出的危害是整数太大了,超出了许可边界。如果整数还没有大到溢出的程度,但也足够大,同样是一个值得警惕的风险。
其他的语言也可能有类似的数据溢出保护方法,欢迎你在评论区留言分享。
### 整数,可能太大
不比数学世界里的整数,软件世界里的整数,大都具有现实的意义。 比如,整数可以代表人民币,可以代表美元,也可以代表文件长度,代表内存空间,代表运算能力。 **一旦抽象的整数被赋予了现实意义,就会有现实的约束**。比如1亿元人民币虽然是个小目标但你要是用来发人手一份的红包也许就有点大了。再比如针对32位整数虽然现代计算机已经可以毫无障碍地表达这个数据了但要是用来分配应用内存这个数就有点大了。
我们要特别警惕大量内存的动态分配。比如说很多协议的设计都会指定待传输数据的大小,而接收端需要按照指定的大小来接收紧接着的数据流。有时候需要分配内存,来存储、处理接收的数据。其中有一种实现,接收到指定大小的数据后,接收端再根据指定的大小分配内存,然后把后续的数据存储在该内存里。指定数据接收完毕,再开始处理该数据。你看出其中的问题了吗?
一个比较典型的安全攻击是攻击者会设置非常大的待传输数据的大小比如2^31)但是只传输非常小的数据比如1个字节然后在很短的时间内发送多个请求一个机器或者多个机器。一个16G内存的服务器如果有8个这样的请求内存就红灯高挂了有10个这样的请求内存可能就要挂免战牌了。这就破坏了服务器的“可用性”算是比较严重的安全事故。
好了,这就是今天的内容,算是关于数的问题的敲门砖,更多的、更深入的话题,可以阅读[CWE-190](https://cwe.mitre.org/data/definitions/190.html),或者留言与我一起讨论。
## 小结
通过对这个评审案例的讨论,我想和你分享下面几点个人看法。
<li>
数值运算,理论结果可能会超出数值类型许可的空间,进而发生实际结果的溢出。
</li>
<li>
抽象的数据一旦有了现实意义,便有了具体的现实约束,我们一定要考虑这些约束。
</li>
<li>
很多问题和我们的习惯并不相符,要通过制度设置来减少由于人的固有缺陷带来的经常性问题。
</li>
## 一起来动手
我们一起讨论了一些整数的问题,你愿不愿意总结下浮点数的问题? 我们使用了C语言和Java语言的示例你了解其他语言关于整数溢出的技术和经验吗
欢迎你来评审下面的这段C语言代码
```
int copy_something(char* buf, int len){
char kbuf[800];
if(len &gt; sizeof(kbuf)){
return -1;
}
return memcpy(kbuf, buf, len);
}
```
或者这段Java代码
```
public static int mixed(int addOn, int multiplied, int scale) {
return addOn + (multiplied * scale);
}
```
或者是下面这段我们已经非常熟悉的Java代码
```
import java.util.HashMap;
import java.util.Map;
class Solution {
/**
* Given an array of integers, return indices of the two numbers
* such that they add up to a specific target.
*/
public int[] twoSum(int[] nums, int target) {
Map&lt;Integer, Integer&gt; map = new HashMap&lt;&gt;();
for (int i = 0; i &lt; nums.length; i++) {
int complement = target - nums[i];
if (map.containsKey(complement)) {
return new int[] { map.get(complement), i };
}
map.put(nums[i], i);
}
throw new IllegalArgumentException(&quot;No two sum solution&quot;);
}
}
```
针对上面三段代码,你有什么改进的建议呢?可以在评论区与我分享你的想法。
另外分享一个最近2019年3月发生的和整数有关的安全事故。
安全起见一个数字证书的序列号应该至少有64位随机数少一位都不行。如果你对整数足够敏感的话就会知道64位是一个特殊的位数。长整型long通常使用64位字节来表述。数字证书的序列号能不能使用64位的长整型呢这就是个坑
为了保证序列号是正数64位的长整型只有63位有效的数字。因为64位长整型中有一位是用来表示数据正负的。所以长整型就不能用做数字证书的序列号。
这个坑就是有人踩了。有数百万张数字证书仅使用了63位的随机数。按照业界规则这些数字证书需要问题发现5天以内撤销重新签发。这几乎是一项不可能完成的任务。2019年3月和4月很多公司都会面临数字证书更新的问题。
如果你觉得这篇文章有所帮助,欢迎点击“请朋友读”,把它分享给你的朋友或者同事,一起来交流。

View File

@@ -0,0 +1,281 @@
<audio id="audio" title="34 | 数组和集合,可变量的安全陷阱" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/8c/3d/8c09bd9360fbb615b4eaeac40573c23d.mp3"></audio>
在前面的章节里,我们讨论了不少不可变量的好处。在代码安全中,不可变量也减少了很多纠葛的发生,可变量则是一个非常难缠的麻烦。
## 评审案例
我们一起看下这段JavaScript代码。
```
var mutableArray = [0, {
toString : function() {
mutableArray.length = 0;
}
}, 2];
console.log(&quot;Array before join(): &quot;, mutableArray);
mutableArray.join('');
console.log(&quot;Array after join(): &quot;, mutableArray);
```
调用mutableArray.join()前后你知道数组mutableArray的变化吗调用join()前数组mutableArray包含两个数字一个函数 {10, {}, 20}。调用join()后数组mutableArray就变成一个空数组了。这其中的秘密就在于join()的实现执行了数组中toString()函数。而toString()函数的实现,把数组mutableArray设置为空数组。
下面的代码就是JavaScript引擎实现数组join()方法的一段内部C代码。
```
static JSBool
array_toString_sub(JSContext *cx, JSObject *obj, JSBool locale,
JSString *sepstr, CallArgs &amp;args) {
// snipped
size_t seplen;
// snipped
StringBuffer sb(cx);
if (!locale &amp;&amp; !seplen &amp;&amp; obj-&gt;isDenseArray() &amp;&amp;
!js_PrototypeHasIndexedProperties(cx, obj)) {
// Elements beyond the initialized length are
// 'undefined' and thus can be ignored.
const Value *beg = obj-&gt;getDenseArrayElements();
const Value *end =
beg + Min(length, obj-&gt;getDenseArrayInitializedLength());
for (const Value *vp = beg; vp != end; ++vp) {
if (!JS_CHECK_OPERATION_LIMIT(cx))
return false;
if (!vp-&gt;isMagic(JS_ARRAY_HOLE) &amp;&amp;
!vp-&gt;isNullOrUndefined()) {
if (!ValueToStringBuffer(cx, *vp, sb))
return false;
}
}
}
// snipped
}
```
这段代码把数组的起始地址记录在beg变量里把数组的结束地址记录在end变量里。然后从beg变量开始通过调用ValueToStringBuffer()函数,把数组里的每一个变量,转换成字符串。
我们一起来看看第一段代码是怎么在这段join()实现的for循环代码里执行的。
<li>
vp指针初始化后指向数组的起始地址
</li>
<li>
如果vp的地址不等于数组的结束地址end就把数组变量转换成字符串然后变换vp指针到下一个地址 。我们一起来看看这段代码是如何操作数组mutableArray的
<p>a. 数组的第一个变量是0。0被转换成字符vp指针换到下一个地址<br>
b. 数组的第二个变量是toString()函数。toString()函数被调用后就会把mutableArray这个数组设置为空数组vp指针换到下一个地址</p>
c. 数组的第三个变量本来应该是2。但是由于数组在上一步被置为空数组数组的第三个变量的指针指向数组外地址。
</li>
<li>
由于数组已经被设置为空数组,原数组的地址可能已经被其他数据占用,访问空数组外的地址就会造成内存泄漏或者程序崩溃。
</li>
通过设置第一段代码里的mutableArray和利用这个内存泄漏的漏洞攻击者可以远程执行任意代码获取敏感信息或者造成服务崩溃。这是一个[通用缺陷评分系统评分为9.9](https://www.first.org/cvss/calculator/3.0#CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:H)的严重安全缺陷。
## 案例分析
我们上面讨论的第一段代码里的mutableArray的构造方式是一个典型的用于检查JavaScript引擎实现或者其他JavaScript数组使用缺陷的技术范例。
近十多年来陆续发现了一些相似的JavaScript引擎数组实现的严重安全漏洞。几乎所有主流的JavaScript引擎提供商都受到了影响。我们太习惯使用数组的编码模式了数组长度的变化很难进入我们的考量范围。因此查看或者编写这些实现代码我们很难发现里面的漏洞除非我们知道了这样的攻击模式。
如果一个新语言支持类似JavaScript语言里这么灵活的函数数组变量你可以试着找找这门编程语言实现里有没有类似的安全漏洞。
如果我们从根本上来看可变量,它的安全威胁就在于**在不同的时间、地点里,可变量可以发生变化。如果编写代码时,意识不到不同时空里的变化,就会面临安全威胁**。
我们再来看一下可变量的例子。在Java语言里java.util.Date是一个从JDK 1.0开始就支持的类。我们可以构建一个对象,来表示构建时的时间,然后再修改成其他时间。就像下面的这段伪代码这样。
```
public void verify(Date targetDate) {
// Verify that a contract is valid in the day of targetDate.
// &lt;snipped&gt;
// Display that the contract is valid in the day of targetDate
}
void checkContract() {
Date today = new Date();
// create a new thread that modify the date to a new date.
// For example, today.setYear(100) will reset the year to 2000.
// verify that the contract is valid today.
veify(today);
}
```
上面的代码中verify()方法和修改日期的线程间就存在竞争关系。如果日期修改在verify()实现的验证和显示之间发生显示的日期就不是验证有效的日期。这就会给人错误的信息从而导致错误的决策。这个问题就是TOCTOUtime-of-check time-of-use竞态危害的一种常见形式。
## 可变量局部化
类似于TOCTOU的安全问题让java.util.Date的使用充满了麻烦。
**那么该怎么防范这种漏洞呢?当然,最有效的方法就是使用不可变量。对于可变量的参数,也可以使用拷贝等办法把共享的变量局部化。由于可变量可以在不同时空发生变化,所以,无论是传入参数,还是返回值,都要拷贝可变量**。这样共享的变量,就转换成了局部变量。
比如上面的例子,我们就可以改成:
```
public void verify(Date targetDate) {
// Create a copy of the targetDate so as to avoid the
// impact of any changes.
Date inputDate = new Date(targetDate.getTime());
// Verify that a contract is valid in the day of inputDate.
// &lt;snipped&gt;
// Display that the contract is valid in the day of inputDate
}
void checkContract() {
Date today = new Date();
// create a new thread that modify the date to a new date.
// For example, today.setYear(100) will reset the year to 2000.
// verify that the contract is valid today.
// Use a clone of the today date so as to avoid the impact of
// any changes in the verify() implementaiton.
veify((Data)today.clone());
}
```
不知道你有没有注意到在veirfy()的实现里我们使用了Date的构造函数来做拷贝而在checkContract()的实现里我们使用了Date的clone()方法来做拷贝。为什么不都使用更简洁的clone()方法呢?
在checkContract()的实现里today变量是Date类的一个实例。我们都了解Date类的clone()方法的实现的确做到了日期的拷贝。而对于作为参数传入verify()方法的targetDate对象我们并不清楚它是不是一个可靠的Date的继承类。这个继承类的clone()实现有没有做日期的拷贝我们也不知晓因此targetDate对象的clone()方法不一定安全可靠。所以在verify()实现里使用clone()拷贝传入的参数,也不可靠。
类的继承还有很多麻烦的地方,后面的章节,我们还会接着讨论继承的安全缺陷。
## 支持实例拷贝
在一定的场景下,安全的编码需要通过拷贝把可变变量局部化。这也就意味着,**我们设计一个可变类的时候,需要考虑支持实例的拷贝。要不然,这个类的使用,可能就会遇到无法安全拷贝的麻烦。**
实例的拷贝可以使用静态的实例化方法或者拷贝构造函数或者使用公开的拷贝方法。需要注意的是如果公开的拷贝方法可以被继承继承类的实现方式就不可预料了。那么这个公开的拷贝方法的使用就是不可靠的。支持公开的拷贝方法一般只适用于final公开类。
静态的实例化:
```
public static MutableClass getInstance(MutableClass mutableObject) {
// snipped
}
```
拷贝构造函数:
```
public MutableClass(MutableClass mutableObject) {
// snipped
}
```
公开拷贝方法:
```
public final class MutableClass {
// snipped
@Override
public Object clone() {
// snipped
}
}
```
禁用拷贝方法:
```
public class MutableClass {
// snipped
@Override
public final Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
}
```
## 浅拷贝与深拷贝
实现拷贝,一般有两种方法。
一种是拷贝变量的指针或者引用并不拷贝变量指向的内容。拷贝和原实例共享指针指向的内容如果拷贝实例里的变量指向的内容发生了改变原实例里的变量指向的内容也随着改变。这种拷贝方法通常称为浅拷贝shallow copy
另外一种方式是拷贝变量指向的内容。拷贝后的实例和原有的实例中变量指向的内容虽然相同但是相互独立的。一个实例里变量指向的内容发生了改变对另外一个实例没有影响。这种拷贝方法通常称为深拷贝deep copy
**对于一个类里的不可变量,一般我们使用浅拷贝就可以了**。这也是不可变量的又一个优点。
下面的这段代码,就混合使用了可变量、不可变量,以及浅拷贝和深拷贝技术,来实现实例拷贝的一个示例。
## 拷贝
```
final class MyContract implements Cloneable {
private String title;
private Date signedDate;
private Byte[] content;
// snipped
@Override
public Object clone() throws CloneNotSupportedException {
MyContract cloned = (MyContract)super.clone();
// shallow copy, String is immutable
cloned.title = this.title;
// deep copy, Date is mutable
cloned.signedDate = new Date(this.signedDate.getTime());
// deep copy, array are mutable
cloned.content = this.content.clone();
return cloned;
}
}
```
**浅拷贝和原实例共享指针指向的内容,拷贝实例和原实例都可以改变指向的内容(除非内容为不可变量),这样就影响了对方的行为。所以,可变量的浅拷贝并不能解除安全隐患。**
由于有两种拷贝方法,而且不同的拷贝方法适用的范围有一定的区别,我们就需要弄清楚一个类支持的是哪一种拷贝方法。特别是如果一个类使用的是浅拷贝,一定要在规范里标记清楚。要不然,就容易用错这个类的拷贝方法,从而导致安全风险。
如果一个类只提供了浅拷贝方法的实现,在使用可变量局部化解决安全隐患时,我们就会遇到很多麻烦。
## 麻烦的集合
出于效率的考虑java.util下的集合类一般支持的是浅拷贝。比如ArrayList的clone()方法,执行的就是浅拷贝。如果使用深度拷贝,在很多场景下,集合的低下效率我们难以承受。对于类似于集合这样的类,可变量局部化就不是一个很好的解决方案。
对于集合来说,我们该怎么解决可变量的竞态危害这个问题呢?最主要的办法,就是不要把集合使用在可能产生竞态危害的场景中,我们后面再接着讨论这个问题。
## 小结
通过对这个案例的讨论,我想和你分享下面三点个人看法。
<li>
**可变量的传递,存在竞态危害的安全风险**
</li>
<li>
**可变量局部化,是解决可变量竞态危害的一种常用办法**
</li>
<li>
**变量的拷贝,有浅拷贝和深拷贝两种形式;可变量的浅拷贝无法解决竞态危害的威胁**
</li>
对于这个案例,你还有什么别的看法吗?
## 一起来动手
数组是一个常见的难以处理的可变量。和集合一样,数组的拷贝也是有损效率的。什么时候,数组的传递需要拷贝?什么时候不需要拷贝?
不管是C语言Java还是JavaScript数组是一个我们编码经常使用的数据类型。你不妨检查一下你的代码看看其中的数组使用是否存在我们上面讨论的安全问题。
欢迎你在留言区分享你的发现。
如果你觉得这篇文章有所帮助,欢迎点击“请朋友读”,把它分享给你的朋友或者同事。

View File

@@ -0,0 +1,193 @@
<audio id="audio" title="35 | 怎么处理敏感信息?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ab/c4/abf32d0e21f6fbb96b25d4e5d257bcc4.mp3"></audio>
敏感信息,是一个常见的词汇。如果我们接收到了广告信息,骚扰电话,垃圾邮件等,都表明我们个人的敏感信息或多或少地被泄露了。
有些敏感信息的泄露,也许仅仅会使我们感到不便,比如一小部分的垃圾邮件,但有些敏感信息的泄露,会影响我们的消费倾向或者消费决策,损害我们的经济利益,甚至威胁我们的生命安全,比如医疗记录和行程安排的泄露。
在计算能力已经不再奢侈的今天看似毫不相干的数据甚至都可以推演出非常关键的隐私信息。比如你每周三次以上在早晨出发的地方大概率就是你家的位置。只有几十个人知道你家的位置这是一件正常的事情可是如果有10亿人知道这就麻烦了几乎不可能发生的小概率事件随时都有可能光临。
我们一定要有保护敏感信息的意识,不管是我们自身的,还是别人的。在互联网世界里,敏感信息真的是无处不在,你知道有哪些编码技术可以保护敏感信息吗?
## 什么是敏感信息?
要想保护敏感信息,首先要识别敏感信息。什么是敏感信息呢? 其实,这个问题本身就是一个特别有意思的话题。你要是查阅关于敏感信息定义的不同文献,就可以体会到不同的立场和不同的利益纠葛。敏感信息的界定范围,也透露了游戏规则制定者对于敏感信息保护的力度和态度。
还记得我们在《[如何评估代码的安全缺陷?](https://time.geekbang.org/column/article/86204)》这篇文章里提到的,信息安全最基本的三个元素吗? 私密性、完整性以及可用性是信息安全的三要素。其中,私密性指的是数据未经授权,不得访问,解决的是“谁能看”的问题。在这个框架下,我们可以把敏感信息理解为,未经授权不得泄漏的信息。反过来说,**未经授权不得泄漏的信息,都算是敏感信息**。
让我们一起来看看最常见的一些敏感信息:
**1.个人敏感信息**
<li>
个人信息:姓名、性别、年龄、身份证号码、电话号码。
</li>
<li>
健康信息:健康记录、服药记录、健康状况。
</li>
<li>
教育记录:教育经历、学习课程、考试分数。
</li>
<li>
消费记录:所购货物、购买记录、支付记录。
</li>
<li>
账户信息:信用卡号、社交账号、支付记录。
</li>
<li>
隐私信息:家庭成员、个人照片、个人行程。
</li>
**2.商业敏感信息**
<li>
商业秘密:设计程序、制作工艺、战略规划、商业模式。
</li>
<li>
客户信息:客户基本信息、消费记录、订单信息、商业合作和合同。
</li>
<li>
雇员信息:雇员基本信息、工资报酬。
</li>
需要注意的是,上述只是一些常见的、直观的敏感信息。具体到你开发的信息系统,到底什么样的信息是敏感信息,什么样的信息不是敏感信息,还需要进一步分析和界定。
识别出敏感信息之后,就要想办法保护这些信息了。**敏感信息的保护,需要恰当的管理,也需要适合的技术。**
## 授权,敏感信息谁能看?
敏感信息指的是未经授权不得泄漏的信息。这个概念可以拆分为三部分:
<li>
敏感信息是受保护的信息,未经授权,不得访问;
</li>
<li>
敏感信息是一段有效信息,有信息处理需求,比如产生、传输、存储、读取、更改、废弃等;
</li>
<li>
敏感信息是有归属的信息,不同的人有不同的权限。经过授权,合适的人可以执行相应的操作。
</li>
**是否需要授权**是敏感信息和普通信息的最关键差异。不同的人有不同的权限,不同的操作需要不同的权限。该如何处理授权呢?
**第一件事情,就是定义权限**。只有定义了权限才能知道如何分配和管理权限。在JDK中权限通过java.security.Permission接口来定义。Permission接口定义权限的名称和操作。比如java.io.FilePermission把权限名定义为文件名把操作定义为
<li>
read
</li>
<li>
write
</li>
<li>
execute
</li>
<li>
delete
</li>
<li>
readlink
</li>
其中,权限的名称就是抽象了的敏感信息;权限的操作就是对信息的处理。如果把权限的名称和权限的操作结合起来,就可以定义特定的权限了。比如,下面的例子就定义了对于文件目录“/home/myhome”的读操作。
```
permission java.io.FilePermission &quot;/home/myhome&quot;, &quot;read&quot;;
```
**第二件事情,就是定义权限的主体**。也就是说要明确权限可以分派给谁。在JDK中权限的主体通过java.security.Principal接口来定义。Principal接口可以用来表述个人组织或者虚拟的账户。比如com.sun.security.auth.UnixPrincipal可以用来表述Unix的用户。
```
Principal com.sun.security.auth.UnixPrincipal &quot;duke&quot;
```
**第三件事情,就是要定义权限的归属**。也就是说,有了权限的定义和权限主体的定义,我们就可以分配权限了。下面的例子,就是把对“/home/duke”的读写操作权限赋予给了Unix用户duke。
```
grant Principal com.sun.security.auth.UnixPrincipal &quot;duke&quot; {
permission java.io.FilePermission &quot;/home/duke&quot;, &quot;read, write&quot;;
};
```
上述的三个例子就是Java权限管理策略文件的最基本概念。更详细的内容权限管理策略文件的语法以及API的调用请参阅有关Java的规范。
敏感信息经过授权才可以使用,这看起来是一个漂亮的解决方案。我们是不是可以高枕无忧了? 这还远远不够。这套解决方案能够实施下去,还是有很多挑战的。比如说,敏感信息的操作处理过程,也会造成信息的非授权泄漏。
## 特殊信息,特殊处理
现代信息系统资源,一般都是多用户共享,多应用共享,跨边界合作的,比如内存、硬盘、中央处理器和互联网。而敏感信息是不能共享的,如何在共享的资源内,不留下敏感信息的踪迹?这是一个让人头疼的问题。
比如说吧,要使用敏感信息,就要把敏感信息载入内存,如果发生内存溢出攻击,攻击者就可以绕过权限管理,获取或者修改敏感信息,甚至可以修改对敏感信息的操作。
**针对敏感信息的操作,需要特殊的处理和特殊的技术。**
敏感信息的无意识泄露是一种比较常见的敏感信息泄露事件。比如说,把敏感信息泄露在抛出异常里,应用日志里,或者序列化对象里。
比如说如果一个文件不存在一般的代码实现会倾向于抛出java.io.FileNotFoundException异常。为了使异常信息更加直观我们常常把文件路径包含在异常的消息里或者记录在应用日志里。
```
java.io.FileNotFoundException /home/duke/.ssh was not found
```
这个异常信息有可能绕过权限管理,传达给未授权用户。这个信息里包含了三个重要的敏感信息:
<li>
当前用户名是“duke”
</li>
<li>
当前用户没有配置SSH协议
</li>
<li>
有可能获知特定文件是否存在。
</li>
当实现一个文件管理类时,我们可能会习惯于面向对象的机制。比如,给定一个文件的路径,代码就执行一定的读写操作。至于该文件路径是什么,包含什么内容,是否有敏感信息,都不在该类的考虑范围之内。实现这个类时,我们更可能倾向于使用直观友好的异常信息,而不会意识到这些**异常信息可能携带敏感数据,导致敏感数据通过异常信息泄露**。
**这和我们一般的面向对象的编程习惯是不符合的,这就要求我们特别小心**。从实现者的角度出发抛出的异常信息尽量做到不包含可能的敏感信息从调用者的角度出发截获的异常信息在传递到上层调用之前如果有必要需要做净化处理。比如把上述的java.io.FileNotFoundException转化成更普通的java.io.IOException。
```
java.io.IOException An IOException was caught!
```
下面的异常堆栈是不是可以接受呢?
```
java.io.IOException An IOException was caught!
at com.example.myapp.MyHTTPSerer.myMethod(MyHTTPSerer.java:250)
...
Caused by java.io.FileNotFoundException /home/duke/.ssh was not found
at com.example.myapp.MyFileStream.open(MyFileStream.java:249)
```
这个异常堆栈的“Caused by”部分泄露了同样的敏感信息。所以**在做异常信息净化处理时,可能还需要避免传递捕获异常的堆栈**。特别是如果调用结果直接面向最终用户就应当尽量避免使用异常堆栈。比如说在HTML页面中显示异常信息和异常堆栈就容易出问题。
在后面的文章中,我们还会讨论敏感信息及时归零的话题。这也是对于高度敏感信息的一种特殊处理方式。
## 小结
通过对这个案例的讨论,我想和你分享下面两点个人看法:
<li>
**要建立主动保护敏感信息的意识;**
</li>
<li>
**要识别系统的敏感信息,并且对敏感信息采取必要的、特殊的处理。**
</li>
保护敏感信息,是编写安全代码的一个重要内容。下一次,我们接着聊更多关于敏感信息的特殊处理技术。
## 一起来动手
阅读隐私保护政策,就是一个建立敏感信息保护意识,学习隐私保护策略和技术的一个好办法。 你可以试着阅读[Google](https://policies.google.com/privacy)和[腾讯](https://privacy.qq.com/)的隐私保护政策。
为了获得相应的服务,作为消费者,我们需要做出什么样的妥协,能得到什么样的保护,我们有什么样的权利?为了提供相应的服务,作为服务者,我们需要什么样的信息,需要多大程度的授权,能够提供什么样的保护?
欢迎你把自己的经验和看法写在留言区,我们一起来学习、思考、精进!
如果你觉得这篇文章有所帮助,欢迎点击“请朋友读”,把它分享给你的朋友或者同事。

View File

@@ -0,0 +1,287 @@
<audio id="audio" title="36 | 继承有什么安全缺陷?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/03/93/03d1c1cd441504b06a53bef54fe21293.mp3"></audio>
有时候,为了解决一个问题,我们需要一个解决办法。可是,这个办法本身还会带来更多的问题。新问题的解决带来更新的问题,就这样周而复始,绵延不绝。
比如[上一篇文章](https://time.geekbang.org/column/article/87256)[](https://time.geekbang.org/column/article/87256),我们说到的敏感信息通过异常信息泄露的问题,就是面向对象设计和实现给我们带来的小困扰。再比如[前面还有](https://time.geekbang.org/column/article/86590)[一个](https://time.geekbang.org/column/article/86590)[案例](https://time.geekbang.org/column/article/86590),说到了共享内存或者缓存技术带来的潜在危害和挑战,这些都是成熟技术发展背后需要做出的小妥协。只是有时候,这些小小的妥协如果没有被安排好和处理好,可能就会带来不成比例的代价。
## 评审案例
我们一起来看一段节选的java.io.FilePermission类的定义。你知道为什么FilePermission被定义为final类吗
```
package java.io;
// &lt;snipped&gt;
/**
* This class represents access to a file or directory. A
* FilePermission consists of a pathname and a set of actions
* valid for that pathname.
* &lt;snipped&gt;
*/
public final class FilePermission
extends Permission implements Serializable {
/**
* Creates a new FilePermission object with the specified actions.
* &lt;i&gt;path&lt;/i&gt; is the pathname of a file or directory, and
* &lt;i&gt;actions&lt;/i&gt; contains a comma-separated list of the desired
* actions granted on the file or directory. Possible actions are
* &quot;read&quot;, &quot;write&quot;, &quot;execute&quot;, &quot;delete&quot;, and &quot;readlink&quot;.
* &lt;snipped&gt;
*/
public FilePermission(String path, String actions);
/**
* Returns the &quot;canonical string representation&quot; of the actions.
* That is, this method always returns present actions in the
* following order: read, write, execute, delete, readlink.
* &lt;snipped&gt;
*/
@Override
public String getActions();
/**
* Checks if this FilePermission object &quot;implies&quot; the
* specified permission.
* &lt;snipped&gt;
* @param p the permission to check against.
*
* @return &lt;code&gt;true&lt;/code&gt; if the specified permission
* is not &lt;code&gt;null&lt;/code&gt; and is implied by this
* object, &lt;code&gt;false&lt;/code&gt; otherwise.
*/
@Override
public boolean implies(Permission p);
// &lt;snipped&gt;
}
```
FilePermission被声明为final也就意味着该类不能被继承不能被扩展了。我们都知道在面向对象的设计中是否具备可扩展性是一个衡量设计优劣的好指标。如果允许扩展的话那么想要增加一个“link”的操作就会方便很多只要扩展FilePermission类就可以了。 但是对于FilePermission这个类OpenJDK为什么放弃了可扩展性
## 案例分析
如果我们保留FilePermission的可扩展性你来评审一下下面的代码可以看出这段代码的问题吗
```
package com.example;
public final class MyFilePermission extends FilePermission {
@Override
public String getActions() {
return &quot;read&quot;;
}
@Override
public boolean implies(Permission p) {
return true;
}
}
```
如果你还没有找出这个问题可能是因为我还遗漏了对FilePermission常见使用场景的介绍。在Java的安全管理模式下一个用户通常可能会被授予有限的权限。 比如用户“xuelei”可以读取用户“duke”的文件但不能更改用户“duke”的文件。
授权的策咯可能看起来像下面的描述:
```
grant Principal com.sun.security.auth.UnixPrincipal &quot;xuelei&quot; {
permission com.example.MyFilePermission &quot;/home/duke&quot;, &quot;read&quot;;
};
```
这项策略要想起作用上面的描述就要转换成一个MyFilePermission的实例。然后调用该实例的implies()方法类判断是否可以授权一项操作。
```
Permission myPermission = ... // read &quot;/home/duke&quot;
public void checkRead() {
if (myPermission.implies(New FilePermission(file, &quot;read&quot;))) {
// read is allowed.
} else {
// throw exception, read is not allowed.
}
}
public void checkWrite() {
if (myPermission.implies(New FilePermission(file, &quot;write&quot;))) {
// writeis allowed.
} else {
// throw exception, write is not allowed.
}
}
```
这里请注意MyFilePermission.implies()总是返回“true” 所以上述的checkRead()和checkWrite()方法总是成功的不管用户被明确指示授予了什么权限实际上暗地里他已经被授予了所有权限。这就成功地绕过了Java的安全管理。
能够绕过Java安全机制的主要原因在于我们允许了FilePermission的扩展。而扩展类的实现有可能有意或者无意地改变了FilePermission的规范和运行从而带来不可预料的行为。
如果你关注OpenJDK安全组的代码评审邮件组你可能会注意到对于面向对象的可扩展性这一便利和诱惑很多工程师能够保持住克制。
保持克制,可能会遗漏一两颗看似近在眼前的甜甜的糖果,但可以减轻你对未来长期的担忧。
一个类或者方法如果使用了final关键字我们可以稍微放宽心。如果没有使用final关键字我们可能需要反复揣摩好长时间仔细权衡可扩展性可能会带来的弊端。
一个公共类或者方法如果使用了final关键字将来如果需要扩展性就可以去掉这个关键字。但是如果最开始没有使用final关键字特别是对于公开的接口来说将来想要加上就可能是一件非常困难的事。
上面的例子是子类通过改变父类的规范和行为带来的潜在问题。那么父类是不是也可以改变子类的行为呢? 这听起来有点怪异,但是父类对子类行为的影响,有时候也的确是一个让人非常头疼的问题。
## 麻烦的继承
我先总结一下,父类对子类行为的影响大致有三种:
<li>
改变未继承方法的实现或者子类调用的方法的实现super
</li>
<li>
变更父类或者父类方法的规范;
</li>
<li>
为父类添加新方法。
</li>
第一种和第三种相对比较容易理解,第二种稍微复杂一点。我们还是通过一个例子来看看其中的问题。
Hashtable是一个古老的被广泛使用的类它最先出现在JDK 1.0中。其中put()和remove()是两个关键的方法。在JDK 1.2中又有更多的方法被添加进来比如entrySet()方法。
```
public class Hashtable&lt;K,V&gt; ... {
// snipped
/**
* Returns a {@link Set} view of the mappings contained in
this map.
* The set is backed by the map, so changes to the map are
* reflected in the set, and vice-versa. If the map is modified
* while an iteration over the set is in progress (except through
* the iterator's own {@code remove} operation, or through the
* {@code setValue} operation on a map entry returned by the
* iterator) the results of the iteration are undefined. The set
* supports element removal, which removes the corresponding
* mapping from the map, via the {@code Iterator.remove},
* {@code Set.remove}, {@code removeAll}, {@code retainAll} and
* {@code clear} operations. It does not support the
* {@code add} or {@code addAll} operations.
*
* @since 1.2
*/
public Set&lt;Map.Entry&lt;K,V&gt;&gt; entrySet() {
// snipped
}
// snipped
}
```
这就引入了一个难以察觉的潜在的安全漏洞。 你可能会问,添加一个方法不是很常见吗?这能有什么问题呢?
问题在于继承Hashtable的子类。假设有一个子类它的Hashtable里要存放敏感数据数据的添加和删除都需要授权在JDK 1.2之前这个子类可以重写put()和remove()方法加载权限检查的代码。在JDK 1.2中这个子类可能意识不到Hashtable添加了entrySet()这个新方法从而也没有意识到要重写覆盖entrySet()方法然而通过对entrySet()返回值的直接操作,就可以执行数据的添加和删除的操作,成功地绕过了授权。
```
public class MySensitiveData extends Hashtable&lt;Object, Object&gt; {
// snipped
@Override
public synchronized Object put(Object key, Object value) {
// check permission and then add the key-value
// snipped
super.put(key, value)
}
@Override
public synchronized Object remove(Object key) {
// check permission and then remove the key-value
// snipped
return super.remove(key);
}
// snipped, no override of entrySet()
}
```
```
MySensitiveData sensitiveData = ... // get the handle of the data
Set&lt;Map.Entry&lt;Object, Object&gt;&gt; sdSet = sensitiveData.entrySet();
sdSet.remove(...); // no permission check
sdSet.add(...); // no permission check
// the sensitive data get modified, unwarranted.
```
现实中,这种问题非常容易发生。一般来说,我们的代码总是依赖一定的类库,有时候需要扩展某些类。这个类库可能是第三方的产品,也可能是一个独立的内部类库。但遗憾的是,类库并不知道我们需要拓展哪些类,也可能没办法知道我们该如何拓展。
所以当有一个新方法添加到类库的新版本中时这个新方法会如何影响扩展类该类库也没有特别多的想象空间和处理办法。就像Hashtable要增加entrySet()方法时让Hashtable的维护者意识到有一个特殊的MySensitiveData扩展是非常困难和不现实的。然而Hashtable增加entrySet()方法,合情又合理,也没有什么值得抱怨的。
然而当JDK 1.0/1.1升级到JDK 1.2时Hashtable增加了entrySet()方法上述的MySensitiveData的实现就存在严重的安全漏洞。要想修复该安全漏洞MySensitiveData需要重写覆盖entrySet()方法,植入权限检查的代码。
可是我们怎样可能知道MySensitiveData需要修改呢 一般来说,如果依赖的类库进行了升级,没有影响应用的正常运营,我们就正常升级了,而不会想到检查依赖类库做了哪些具体的变更,以及评估每个变更潜在的影响。这实在不是软件升级的初衷,也远远超越了大部分组织的能力范围。
而且如果MySensitiveData不是直接继承Hashtable而是经过了中间环节这个问题就会更加隐晦更加难以察觉。
```
public class IntermediateOne extends Hashtable&lt;Object, Object&gt;;
public class IntermediateTwo extends IntermediateOne;
public class Intermediate extends IntermediateTwo;
public class MySensitiveData extends Intermediate;
```
糟糕的是,随着语言变得越来越高级,类库越来越丰富,发现这些潜在问题的难度也是节节攀升。我几乎已经不期待肉眼可以发现并防范这类问题了。
那么,到底有没有办法可以防范此类风险呢?
主要有两个方法。
**一方面,当我们变更一个可扩展类时,要极其谨慎小心**。一个类如果可以不变更就尽量不要变更能在现有框架下解决问题就尽量不要试图创造新的轮子。有时候我们的确难以压制想要创造出什么好东西的冲动这是非常好的品质。只是变更公开类库时一定要多考虑这么做的潜在影响。你是不是开始思念final关键字的好处了
**另一方面,当我们扩展一个类时,如果涉及到敏感信息的授权与保护,可以考虑使用代理的模式,而不是继承的模式**。代理模式可以有效地降低可扩展对象的新增方法带来的影响。
```
public class MySensitiveData {
private final Hashtable hashtable = ...
public synchronized Object put(Object key, Object value) {
// check permission and then add the key-value
hashtable.put(key, value)
}
public synchronized Object remove(Object key) {
// check permission and then remove the key-value
return hashtable.remove(key);
}
}
```
我们使用了Java语言来讨论继承的问题其实**这是一个面向对象机制的普遍的问****题,**甚至它也不单单是面向对象语言的问题比如使用C语言的设计和实现也存在类似的问题。
## 小结
通过对这个案例的讨论,我想和你分享下面两点个人看法。
<li>
**一个可扩展的类,子类和父类可能会相互影响,从而导致不可预知的行为。**
</li>
<li>
**涉及敏感信息的类,增加可扩展性不一定是个优先选项,要尽量避免父类或者子类的影响。**
</li>
学会处理和保护敏感信息,是一个优秀工程师必须迈过的门槛。
## 一起来动手
了解语言和各种固定模式的缺陷,是我们打怪升级的一个很好的办法。有时候,我们偏重于学习语言或者设计经验的优点,忽视了它们背后做出小小的妥协,或者缺陷。如果能利用好优点,处理好缺陷,我们就可以更好地掌握这些经验总结。毕竟世上哪有什么完美的东西呢?不完美的东西,用好了,就是好东西。
我们利用讨论区,来聊聊设计模式这个老掉牙的、备受争议的话题。说起“老掉牙”,科技的进步真是快,设计模式十多年前还是一个时髦的话题,如今已经不太受待见了,虽然我们或多或少,或直接或间接地都受益于设计模式的思想。如果你了解过设计模式,你能够分享某个设计模式的优点和缺陷吗? 使用设计模式有没有给你带来实际的困扰呢?
上面的例子中,我们提到了使用代理模式来降低父类对子类的影响。那么你知道代理模式的缺陷吗?
欢迎你把自己的经验和看法写在留言区,我们一起来学习、思考、精进!
如果你觉得这篇文章有所帮助,欢迎点击“请朋友读”,把它分享给你的朋友或者同事。

View File

@@ -0,0 +1,358 @@
<audio id="audio" title="37 | 边界,信任的分水岭" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/c4/ad/c44e49e18885ea018f90f6061af52fad.mp3"></audio>
边界是信息安全里一个重要的概念。如果不能清晰地界定信任的边界,并且有效地守护好这个边界,那么编写安全的代码几乎就是一项不可能完成的任务。
## 评审案例
计算机之间的通信,尤其是建立在非可靠连接之上的通信,如果我们能够知道对方是否处于活跃状态,会大幅度地提升通信效率。在传输层安全通信的场景下,这种检测对方活跃状态的协议,叫做心跳协议。
心跳协议的基本原理,就是发起方给对方发送一段检测数据,如果对方能原封不动地把检测数据都送回,就证明对方处于活跃状态。
下面的数据结构,定义的就是包含检测数据的通信消息。
```
struct {
HeartbeatMessageType type;
uint16 payload_length;
opaque payload[HeartbeatMessage.payload_length];
opaque padding[padding_length];
} HeartbeatMessage;
```
其中type是一个字节表明心跳检测的类型payload_length使用两个字节定义的是检测数据的长度payload的字节数由payload_length确定它携带的是检测数据padding是随机的填充数据最少16个字节。
如果愿意回应心跳请求接收方就拷贝检测数据payload_length和payload并把它封装在同样的数据结构里。
下面的这段代码函数process_heartbeat为便于阅读在源代码基础上有修改就是接收方处理心跳请求的C语言代码。你能看出其中的问题吗
```
int process_heartbeat(
unsigned char* request, unsigned int request_length) {
unsigned char *p = request, *pl;
unsigned short hbtype;
unsigned int payload_length;
unsigned int padding_length = 16; /* Use minimum padding */
/* Read type and payload length first */
hbtype = *p++;
payload_length = ((unsigned int)(*p++)) &lt;&lt; 8L |
((unsigned int)(*p++));
pl = p;
// produce response heaetbeat message
unsigned char *response, *bp;
/* Allocate memory for the response, size is 1 bytes
* message type, plus 2 bytes payload length, plus
* payload, plus padding
*/
response = malloc(1 + 2 + payload_length + padding_length);
bp = response;
/* Enter response type, length and copy payload */
*bp++ = 1; /* 1: response heartbeat type */
*bp++ = (unsigned char)((payload_length &gt;&gt; 8L) &amp; 0xff);
*bp++ = (unsigned char)((payload_length ) &amp; 0xff);
memcpy(bp, pl, payload_length);
bp += payload_length;
// snipped
return 0;
}
```
上面这段代码读取了请求的payload_length字段然后按照payload_length的大小分配了一段内存。然后从请求数据的payload指针开始拷贝了和payload_length一样大小的一段数据。这段数据就是要回应给请求方的检测数据。 按照协议,这段数据应该和请求信息的检测数据一模一样。
比如说吧,如果心跳请求的数据是:
```
type: 0x01
payload_length: 0x00, 0x05 // 5
payload: {0x68, 0x65, 0x6c, 0x6c, 0x6f}; // 'hello'
padding: {0xCF, 0xED, ...};
```
按照协议和上面实现的代码,心跳请求的回应数据应该是:
```
type: 0x01
payload_length: 0x00, 0x05 // 5
payload: {0x68, 0x65, 0x6c, 0x6c, 0x6f}; // 'hello'
padding: {0x07, 0x91, ...};
```
这看起来很美好,是吧? 可是如果请求方心有图谋在心跳请求数据上动了手脚问题就来了。比如说吧还是类似的心跳请求但是payload_length的大小和真实的payload大小不相符合。下面的这段请求数据检测数据还是只有5个字节但是payload_length字段使用了一个大于5的数字。
```
type: 0x01
payload_length: 0x04, 0x00 // 1024
payload: {0x68, 0x65, 0x6c, 0x6c, 0x6f}; // hello
padding: {0xCF, 0xED, ...};
```
按照协议的本意,这不是一个合法的心跳请求。上面处理心跳请求的代码,不能识别出这是一个不合法的请求,依旧完成了心跳请求的回应。
```
type: 0x01
payload_length: 0x04, 0x00 // 1024
payload: {0x68, 0x65, 0x6c, 0x6c, 0x6f, // 'hello
0xCF, 0xED, ... // request padding
0x70, 0x72, 0x69, 0x76, 0x69, 0x76, 0x61, 0x74,
0x65, 0x20, 0x6b, 0x65, 0x79, 0x20,
... }; // private key &quot;...&quot;
padding: {0x07, 0x91, ...};
```
心跳请求的真实检测数据只有5个字节返回检测数据有1024个字节这中间有1019个字节的差距。这1019个字节从哪儿来呢由于代码使用了memcpy()函数这1019个字节就是从payload指针pl后面的内存中被读取出来的。这些内存中可能包含很多敏感信息比如密码的私钥用户的社会保障号等等。
这就是著名的心脏滴血漏洞Heartbleed这个漏洞出现在OpenSSL的代码里。2014年4月7日OpenSSL发布了这个漏洞的修复版。由于OpenSSL的广泛使用有大批的产品和服务需要升级到修复版而升级需要时间。修复版刚刚发布像猎食者一样的黑客抢在产品和服务的升级完成之前马上就展开了攻击。赛跑立即展开仅隔一天2014年4月8日加拿大税务局遭受了长达6个小时的攻击大约有900人的社会保障号被泄漏。2014年4月14日英国育儿网站Mumsnet有几个用户帐户被劫持其中包括了其首席执行官的账户。2014年8月一家世界500强医疗服务机构透露心脏滴血漏洞公开一周后他们的系统遭受攻击导致四百五十万条医疗数据被泄漏。
<img src="https://static001.geekbang.org/resource/image/9d/e2/9d96cea6b0aefe50fc77640c56652ce2.png" alt=""><br>
【图片来自[http://heartbleed.com/](http://heartbleed.com/) [https://en.wikipedia.org/wiki/Heartbleed#/media/File:Heartbleed.svg](https://en.wikipedia.org/wiki/Heartbleed#/media/File:Heartbleed.svg)】
## 案例分析
没有检查和拒绝不合法的请求,是心脏滴血漏洞出现的根本原因。这个漏洞的修复也很简单,增加检查心跳请求的数据结构是否合法的代码就行了。
下面的代码就是修复后的版本。修复后的代码加入了对心跳请求payload_length的检查。
```
int process_heartbeat(
unsigned char* request, unsigned int request_length) {
unsigned char *p = request, *pl;
unsigned short hbtype;
unsigned int payload_length;
unsigned int padding_length = 16; /* Use minimum padding */
/* Read type and payload length first */
if (1 + 2 + 16 &gt; request_length) {
/* silently discard */
return 0;
}
hbtype = *p++;
payload_length = ((unsigned int)(*p++)) &lt;&lt; 8L |
((unsigned int)(*p++));
if (1 + 2 + payload_length + 16 &gt; request_length) {
/* silently discard */
return 0;
}
pl = p;
// produce response heaetbeat message
unsigned char *response, *bp;
/* Allocate memory for the response, size is 1 bytes
* message type, plus 2 bytes payload length, plus
* payload, plus padding
*/
response = malloc(1 + 2 + payload_length + padding_length);
bp = response;
/* Enter response type, length and copy payload */
*bp++ = 1; /* 1: response heartbeat type */
*bp++ = (unsigned char)((payload_length &gt;&gt; 8L) &amp; 0xff);
*bp++ = (unsigned char)((payload_length ) &amp; 0xff);
memcpy(bp, pl, payload_length);
bp += payload_length;
// snipped
return 0;
}
```
如果比较下process_heartbeat()函数修复前后的实现代码,我们就会发现修复前的危险性主要来自于两点:
<li>
没有检查外部数据的合法性payload_length和payload
</li>
<li>
内存的分配和拷贝依赖于外部的未校验数据malloc和memcpy
</li>
这两点都违反了一条基本的安全编码原则,我们在前面提到过这条原则,那就是:[跨界的数据不可信任](https://time.geekbang.org/column/article/85968)。
## 信任的边界
不知道你有没有这样的疑问类似于memcpy()函数如果process_heartbeat()函数的传入参数request_length的数值大于传入参数request实际拥有的数据量这个函数不是还有内存泄漏问题吗
如果独立地看上面的代码这样的问题是有可能存在的。但是process_heartbeat()是OpenSSL的一个内部函数它的调用代码已经检查过request容量和request_length的匹配问题。所以在process_heartbeat()的实现代码里,我们就不再操心这个匹配的问题了。
对一个函数来说,到底哪些传入参数应该检查,哪些传入参数不需要检查?这的确是一个让人头疼的问题。
一般来说,对于代码内部产生的数据,我们可以信任它们的合法性;而对于外部传入的数据,就不能信任它们的合法性了。外部数据,需要先检验,再使用。
**区分内部数据、外部数据的依据,就是数据的最原始来源,而不是数据在代码中的位置。**
比如下面的示意图,标明的就是一些典型的数据检查点。 其中小写字母代表数据,大写字母标示的方框代表函数或者方法,数字代表检查点,箭头代表数据流向。
<img src="https://static001.geekbang.org/resource/image/6d/72/6d33f116877c904d52e0a101b70b9872.png" alt="">
<li>
数据a是一个外部输入数据函数A使用数据a之前需要校验它的合法性检查点1
</li>
<li>
数据b是一个外部输入数据函数A使用数据b之前完全校验了它的合法性检查点2。函数A内部调用的函数B在使用数据b时就不再需要检查它的合法性了。
</li>
<li>
数据c是一个外部输入数据函数A使用数据c之前部分校验了它的合法性检查点3。函数A只能使用校验了合法性的部分数据。函数A内部调用的函数B在使用数据c时如果需要使用未被检验部分的数据还要检查它的未被校验部分的合法性检查点4
</li>
<li>
数据d是一个外部输入数据函数A使用数据d之前部分校验了它的合法性检查点5。函数A内部调用的函数B没有使用该数据但是把该数据传送给了函数C。函数C在使用数据d时如果需要使用未被检验部分的数据还要检查它的未被校验部分的合法性检查点6
</li>
<li>
数据e和f是一个内部数据函数C使用内部数据时不需要校验它的合法性。
</li>
<li>
数据g是一个内部数据由函数A产生并且输出到外部。这时候不需要检验数据g的合法性但是需要防护输出数据的变化对内部函数A状态的影响防护点7
</li>
原则上,对于外部输入数据的合法性,我们要尽早校验,尽量全面校验。但是有时候,只有把数据分解到一定程度之后,我们才有可能完成对数据的全面校验,这时候就比较容易造成数据校验遗漏。
我们上面讨论过的心脏滴血漏洞就有点像数据d的用例调用关系多了几层数据校验的遗漏就难以察觉了。
## 哪些是外部数据?
你是不是还有一个疑问为什数据e和f对函数C来说就不算是外部数据了它们明明是函数C的外部输入数据呀
当我们说跨界的数据时这些数据指的是一个系统边界外部产生的数据。如果我们把函数A、函数B和函数C看成一个系统那么数据e和数据f就是这个系统边界内部产生的数据。内部产生的数据一般是合法的要不然就存在代码的逻辑错误内部产生的数据一般也是安全的不会故意嵌入攻击性逻辑。所以为了编码和运行的效率我们一般会选择信任内部产生的数据。
一般的编码环境下,我们需要考量四类外部数据:
<li>
用户输入数据(配置信息、命令行输入,用户界面输入等);
</li>
<li>
I/O输入数据TCP/UDP连接文件I/O
</li>
<li>
公开接口输入数据;
</li>
<li>
公开接口输出数据。
</li>
我想,前三类外部数据都容易理解。第四类公开接口输出数据,不是内部数据吗?怎么变成需要考量的外部数据了?我们在[前面的章节](https://time.geekbang.org/column/article/87077)讨论过这个问题。
公开接口的输出数据,其实是把内部数据外部化了。如果输出数据是共享的可变量(比如没有深拷贝的集合和数组),那么外部的代码就可以通过修改输出数据,进而影响原接口的行为。这也算是一种意料之外的“输入”。
需要注意的是,公开接口的规范,要标明可变量的处理方式。要不然,调用者就不清楚可不可以修改可变量。
让调用者猜测公开接口的行为,会埋下兼容性的祸根。
比如下面的例子就是两个Java核心类库的公开方法。这两个方法对于传入、传出的可变量数组都做了拷贝并且在接口规范里声明了变量拷贝。
```
package javax.net.ssl;
// snipped
public class SSLParameters {
private String[] applicationProtocols = new String[0];
// snipped
/**
* Returns a prioritized array of application-layer protocol names
* that can be negotiated over the SSL/TLS/DTLS protocols.
* &lt;snipped&gt;
* This method will return a new array each time it is invoked.
*
* @return a non-null, possibly zero-length array of application
* protocol {@code String}s. The array is ordered based
* on protocol preference, with {@code protocols[0]}
* being the most preferred.
* @see #setApplicationProtocols
* @since 9
*/
public String[] getApplicationProtocols() {
return applicationProtocols.clone();
}
/**
* Sets the prioritized array of application-layer protocol names
* that can be negotiated over the SSL/TLS/DTLS protocols.
* &lt;snipped&gt;
* @implSpec
* This method will make a copy of the {@code protocols} array.
* &lt;snipped&gt;
* @see #getApplicationProtocols
* @since 9
*/
public void setApplicationProtocols(String[] protocols) {
if (protocols == null) {
throw new IllegalArgumentException(&quot;protocols was null&quot;);
}
String[] tempProtocols = protocols.clone();
for (String p : tempProtocols) {
if (p == null || p.isEmpty()) {
throw new IllegalArgumentException(
&quot;An element of protocols was null/empty&quot;);
}
}
applicationProtocols = tempProtocols;
}
}
```
从上面的例子中,我们也可以体会到,公开接口的编码要比内部接口的编码复杂得多。因为我们无法预料接口的使用者会怎么创造性地使用这些接口。公开接口的实现一般要慎重地考虑安全防护措施,这让公开接口的设计、规范和实现都变得很复杂。从这个意义上来说,我们也需要遵守在第二部分“经济的代码”里谈到的原则:[接口要简单直观](https://time.geekbang.org/column/article/82605)。
## 小结
通过对这个案例的讨论,我想和你分享下面两点个人看法。
<li>
**外部输入数据,需要检查数据的合法性;**
</li>
<li>
**公开接口的输入和输出数据,还要考虑可变量的传递带来的危害。**
</li>
## 一起来动手
外部数据的合法性问题,是信息安全里的一大类问题,也是安全攻击者经常利用的一类安全漏洞。
**区分内部数据、外部数据的依据,是数据的最原始来源,而不是数据在代码中的位置。**这一点让外部数据的识别变得有点艰难,特别是代码层数比较多的时候,我们可能没有办法识别一个传入参数,到底是内部数据还是外部数据。在这种情况下,我们需要采取比较保守的姿态,**无法识别来源的数据,不应该是可信任的数据。**
这一次的练习题,我们按照保守的姿态,来分析下面这段代码中的数据可信任性问题。
```
import java.util.HashMap;
import java.util.Map;
public class Solution {
/**
* Given an array of integers, return indices of the two numbers
* such that they add up to a specific target.
*/
public int[] twoSum(int[] nums, int target) {
Map&lt;Integer, Integer&gt; map = new HashMap&lt;&gt;();
for (int i = 0; i &lt; nums.length; i++) {
int complement = target - nums[i];
if (map.containsKey(complement)) {
return new int[] { map.get(complement), i };
}
map.put(nums[i], i);
}
throw new IllegalArgumentException(&quot;No two sum solution&quot;);
}
}
```
欢迎你把你的看法写在留言区,我们一起来学习、思考、精进!
如果你觉得这篇文章有所帮助,欢迎点击“请朋友读”,把它分享给你的朋友或者同事。
<img src="https://static001.geekbang.org/resource/image/92/57/92312caf60969f2bbe05a09307e1ac57.jpg" alt="">

View File

@@ -0,0 +1,180 @@
<audio id="audio" title="38 | 对象序列化的危害有多大?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/b5/ab/b5fb87bea9d17df6d812c10871166bab.mp3"></audio>
如果一个函数或者对象不管它位于多么遥远的地方都可以在本地直接被调用那该有多好呀这是一个非常朴素、美好的想法。基于这个设想诞生了很多伟大的技术和协议比如远程过程调用RPC、远程方法调用RMI、分布式对象Distributed Object、组件对象模型COM、公共对象请求代理CORBA和简单对象访问协议SOAP等……这个列表还可以很长很长。
躲在这些协议背后的核心技术之一,就是**序列化**。简单地说,序列化就是要把一个使用场景中的一个函数或者对象以及它们的执行环境,打包成一段可以传输的数据,然后把该数据传输给另外一个使用场景。在这个使用场景中,该数据被拆解成适当的函数或者对象,包括该函数或者对象的执行环境。这样,该函数或者对象就可以在不同的场景下使用了。
**数据拆解的过程,就是反序列化**。**打包、传输、拆解是序列化技术的三个关键步骤**。由于传输的是数据,打包和拆解可能使用不同的编程语言,运行在不同的操作系统上。这样就带来了跨平台和跨语言的好处。而数据能够传输,就意味着可以带来分布式的好处。数据当然也可以存储,而可以存储意味着相关对象的生命周期的延长,这是不是也是一个非常值得兴奋的特点?
的确是一个美妙的想法,对吧? **如果一个想法不是足够好,它也不会造成足够坏的影响。**
## 评审案例
我们用Java语言的例子来看看序列化的问题。先一起来看一段节选的Java代码。你能看出这段代码有什么问题吗该怎么解决这个问题
```
public class Person implements Serializable {
// &lt;snipped&gt;
private String firstName;
private String lastName;
private String birthday;
private String socialSecurityNumber;
public Person(String firstName, String lastName,
String birthday, String socialSecurityNumber) {
this.firstName = firstName;
this.lastName = lastName;
this.birthday = birthday;
this.socialSecurityNumber = socialSecurityNumber;
}
// &lt;snipped&gt;
}
```
注意socialSecurityNumber表示社会保障号是一个高度敏感、需要高度安全保护的数据。如果社会保障号以及姓名、生日等信息被泄露那么冒名顶替者就可以用这个号码举债买房、买车而真实用户则要背负相关的债务。一旦社会保障号被泄露想要证明并不是你申请了贷款远远不是一件轻而易举的事情。在有些国家社会保障号的保护本身甚至都是一个不小的生意。 在一个信息系统中,除了本人以及授权用户,任何其他人都不应该获知社会保障号以及相关的个人信息。
上述的代码,存在泄露社会保障号以及相关的个人信息的巨大风险。
## 案例分析
打包、传输、拆解是序列化技术的三个关键步骤。我们来分别看看这三个步骤。
首先打包环节会把一个Person实例里的姓名、生日、社会保障号等信息转化为二进制数据。这段数据可以被传输、存储和拆解。任何人看到这段二进制数据都可以拆解还原成一个Person实例从而获得个人敏感信息。这段二进制数据在传输和存储的过程中有可能被恶意的攻击者修改从而影响Person实例的还原。如果这个实例涉及到具体的商业交易那么通过这样的攻击还可以修改交易对象。
你看,序列化后的每一个环节,都有可能遭受潜在的攻击。序列化的问题有多严重呢?据说,**大约有一半的Java漏洞和序列化技术有直接或者间接的关系**。而且,由于序列化可以使用的场景非常多,序列化对象既可以看又可以改,这样就导致序列化安全漏洞的等级往往非常高,影响非常大。甚至每年都会有公司专门收集、整理和分析序列化漏洞,这就加剧了序列化安全漏洞的影响,特别是对于那些没有及时修复的系统来说。
1997年Java引入序列化技术至今二十多年里由于序列化技术本身的安全问题Java尝尽了其中的酸楚。这是一个“美妙”的想法带来的可怕错误。如果有一天Java废弃了序列化技术那一点儿也不值得惊讶。毕竟和得到的好处相比要付出的代价实在是太沉重了
如果你的应用还没有开始使用序列化技术,这很好,**不要惦记序列化的好处,坚持不要使用序列化**。如果你的应用已经使用了序列化技术,那么可以做些什么来防范或者降低序列化的风险呢?
## 额外的防护
序列化技术本身并没有内在的安全防护措施这也是Java序列化为什么会这么令人诅丧的原因之一。如果一定要使用序列化技术我们就需要设计、部署、加固序列化的安全防线。
我们先聊聊面对序列化带来的种种问题,该如何保护被序列化的敏感数据。
**首先推荐的方式是,含有敏感数据的类,不要支持序列化**。当然,这也就主动放弃了序列化带来的好处。
**次优的方式是,不要序列化敏感数据,把敏感数据排除在序列化数据之外**。比如,案例中的序列化数据可以抽象地表述为如下的四项:
```
firstName | lastName | birthday | socialSecurityNumber
```
能不能把敏感的socialSecurityNumber和birthday排除在外呢Java语言的关键字transient就是为这一功能设计的。
```
public class Person implements Serializable {
// &lt;snipped&gt;
private String firstName;
private String lastName;
private transient String birthday; // sensitive data
private transient String socialSecurityNumber; // sensitive data
public Person(String firstName, String lastName,
String birthday, String socialSecurityNumber) {
this.firstName = firstName;
this.lastName = lastName;
this.birthday = birthday;
this.socialSecurityNumber = socialSecurityNumber;
}
// &lt;snipped&gt;
}
```
如果把socialSecurityNumber和birthday变量声明为transient对象实例的序列化就会把这两个变量排除在外。这个时候序列化数据就不包含敏感数据了。
```
firstName | lastName
```
**排除敏感数据的序列化,还有另一种办法,那就是指定可以序列化的非敏感数据**。如果把transient关键字提供的变量声明看成一个黑名单模式Java还提供了一个白名单模式。使用静态的serialPersistentFields变量可以指定哪些变量可以序列化。上面的案例中如果只序列化firstName和lastName变量那么敏感的socialSecurityNumber和birthday变量自然就被排除在外了。
```
public class Person implements Serializable {
// &lt;snipped&gt;
private String firstName;
private String lastName;
private String birthday; // sensitive data
private String socialSecurityNumber; // sensitive data
// list of serializable fields
private static final ObjectStreamField[]
serialPersistentFields = {
new ObjectStreamField(&quot;firstName&quot;, Person.class),
new ObjectStreamField(&quot;lastName&quot;, Person.class)
};
// &lt;snipped&gt;
}
```
可是,如果把敏感数据排除在序列化数据之外,也就意味着敏感数据不会在拆解后的对象实例中出现。这就使得序列化之前的实例和反序列化之后的实例并不一致。这种差异的存在,就足以使得序列化名存实亡,反序列化后的对象实例可能就没有太多的实际意义了。
那么有没有一种方法,既可以保护敏感数据,也能保持对象实例序列化前后的等价呢?办法还是有的。
如果在一个完全可信任的环境下,既不用担心敏感信息的泄露,也不用担心敏感信息的修改,更不用担心对象会被用于非可信的环境,敏感数据可以正常实例化了。然而,这严重限制了对象的使用环境,如果用错了环境,就会面临严肃的安全问题。
如果对象有可能适用于非可信的环境,就要使用复杂一些的技术。比如使用加密和签名技术,解决“谁能看”和“谁能改”的安全问题。可是,复杂技术的使用,几乎意味着我们对性能要求做出了妥协。面对这样的妥协,是否还需要使用序列化,有时候也是一个两难的选择。
## 小结
通过对这个评审案例的讨论,我想和你分享下面两点个人看法。
<li>
**序列化技术不是一个有安全保障的技术,序列化数据的传输和拆解过程都可能被攻击者利用**
</li>
<li>
**要尽量避免敏感信息的序列化**
</li>
除了上述我们说到的方法,敏感信息在序列化过程中的处理和保护,还有三种常见的方法:
<li>
实现writeObject主动地、有选择地序列化指定数据。writeObject和serialPersistentFields变量都是指定序列化数据但区别在于writeObject()覆盖了序列化的缺省函数,所以编码可以更自由;
</li>
<li>
实现writeReplace 使用序列化代理;
</li>
<li>
实现Externalizable接口。
</li>
我们把这三种方法的使用,留给讨论区,欢迎你对这三种方法做总结、分析,并与我一起交流。
## 一起来动手
下面的这段Java代码有一个隐藏的序列化安全问题。你能找到这个问题并且解决掉这个问题吗
```
public class Person extends HashMap&lt;String, String&gt; {
// &lt;snipped&gt;
public Person(String firstName, String lastName,
String birthday, String socialSecurityNumber) {
super();
super.put(&quot;firstName&quot;, firstName);
super.put(&quot;lastName&quot;, lastName);
super.put(&quot;birthday&quot;, birthday);
super.put(&quot;socialSecurityNumber&quot;, socialSecurityNumber);
}
// &lt;snipped&gt;
}
```
欢迎你把自己看到的问题和想到的解决方案写在留言区,我们一起来学习、思考、精进!
如果你觉得这篇文章有所帮助,欢迎点击“请朋友读”,把它分享给你的朋友或者同事。

View File

@@ -0,0 +1,240 @@
<audio id="audio" title="39 | 怎么控制好代码的权力?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/e8/b4/e81b01aca4459002c10b9723aebd84b4.mp3"></audio>
在前面,我们讨论了“敏感信息经过授权才可以使用”的这样一条有关编码安全的实践。我们还可以把这个实践扩展到更大的范围:信息和资源,需经授权,方可使用。这个信息和资源,不仅仅包括用户数据这样的敏感信息,还包括计算机代码、产品和服务。
授权使用这些资源,需要遵循“最小授权”的原则。所授予的权力,能够让应用程序完成对应的任务就行,不要授予多余的权力。为了方便,我们可以把“最小授权”这个概念拆分成如下的两个部分来理解:
<li>
最小权力的设计
</li>
<li>
最小限度的授予
</li>
## 最小权力的设计
其实,不管使用什么编程语言,我们编写的代码都会涉及到代码权力的设计。最常见的设计,就是代码的访问控制权限的设计。
一段代码访问应用程序接口的过程,一般需要至少两个步骤,第一步是加载类库,第二步是调用接口。这两个步骤,都需要设计好访问控制权限。
### 模块的访问权限
下面的例子就是一个Java模块的权限设计module-info.java。这个权限设计定义了一个example.coding模块。这个模块允许外部代码使用它提供的com.example.coding内部接口。
```
module example.coding {
exports com.example.coding;
}
```
这个模块可能还包含其他的接口比如位于com.example.implement包内的代码。由于模块的定义没有允许外部代码使用除了com.example.coding包空间以外的接口那么com.example.implement包内的接口即便是public接口外部代码也不能直接访问了。
这个模块被加载时,它可以接受的访问控制权限也就相应地确定了。
我们在设计一个模块时,需要尽量把命名空间设计好,开放的接口放在一个开放的包里;内部接口或者代码实现,放在封闭的包里。把开放的部分和封闭的部分,分割开来。这样我们就设计了一道安全的边界,开放包里的代码,经过精心设计和耐心打磨,处理好潜在的安全问题。而封闭包里的代码编写就少了很多安全的顾虑,可以让编写更有效率。
这样的设计,也使得这个模块和外部的接触面更小。接触面越小,代码的安全问题就越少,代码的接口就越容易管理。
模块化是JDK 9引入的一个新特性。
在JDK 9之前有很多声明为public的内部类比如com.sun.net.internal包里的类。虽然这些内部的类声明为public但是它们的真实意图往往是方便内部不同包内的接口共享而不是开放给外部的应用程序使用。所以Java的文档会一再强调应用程序不要使用内部类即使这些类声明为public。因为这些内部类可能随时被改变随时被删除。另外内部类一般也没有规范的文档实现的代码依赖内部假设使用场景严格受限这也让这些类的使用充满了陷阱。
然而这些内部的public类毕竟有它们的价值和便利的地方一些应用为了方便使用了内部类。这不仅给内部类的修改带来了很大的困扰也让应用程序面临不安定的兼容性和安全性问题。
Java的模块化这个特性通过增加一个访问控制边界更好地区分开了开放和封闭的空间提高了代码的安全性和可维护性。
### 接口的访问权限
Java接口的访问控制权限是由我们熟知的三个修饰符来定义的。这三个修饰符就是public、 protected和private。如果三个修饰符都不使用那就是缺省的访问控制权限。如果加上缺省的权限那么Java的访问控制权限可以分为四类。
这四类权限定义接口的访问控制,具体可以参考下面的表格。
<img src="https://static001.geekbang.org/resource/image/b6/0d/b623f01eab659eab66e5be94f129100d.png" alt=""><br>
掌握这四类权限是Java编码的基本功我们都很熟悉这里我们强调的是它们的使用优先级。
在我们日常的编码中需要遵循“优先最小权限”的原则。也就是说应该优先使用权限最小的方案。按照这样的原则Java接口的访问控制权限的使用优先级从高到低的顺序是
<li>
private
</li>
<li>
缺省的权限
</li>
<li>
protected
</li>
<li>
public
</li>
这需要我们养成一个习惯遇到不是private的接口我们一定要想一想这个接口可以改成private接口吗如果不能接口需要的最小访问控制权限是什么我们还可以做些什么事情来降低这个接口的权限减小接口的开放程度
掌握Java接口的访问控制权限虽然是Java编码基本功之一但要真的用好落实到设计和编码上也不是一件容易的事情。由于在编码过程中我们往往会集中精力在代码的业务逻辑上忽视了代码权限控制的概念。在OpenJDK的代码评审中经常可以看到访问控制权限使用的疏忽。即使是对于资深的工程师而言这也是一个常见的编码疏漏。
Java接口的访问控制权限是我们可以设置、使用的另外一道安全边界。这道边界把类、包、子类以及外部代码区隔开来。**越开放的权限越需要控制,越封闭的权限越容易维护**。
### 修改的权限
还有一类权限不太容易引起我们的注意。它就是修改的权限。在编程语言语法层面Java语言中这个权限由final修饰符来定义而C语言使用const关键字。
final的类和方法不允许被继承阻断了代码实现的修改final的变量不允许被修改阻断了使用者带来的变更。我们前面讨论过可变量的威胁和继承的危害限制修改权限是规避这两类陷阱的最有效办法。
final类
```
private final class Foo {
// snipped
}
```
final方法
```
private class Foo {
// snipped
final InputStream getInputStream() {
// snipped
}
}
```
final变量
```
private final class Foo {
private final Socket socket;
// snipped
}
```
同样的编码的时候我们也要养成限制修改权限的习惯能使用final修饰符的地方就使用final修饰符没有使用final修饰符的地方可以想一想使用final修饰符能不能带来代码的改进不能使用final修饰符的地方想一想有没有可变量和继承的陷阱如果存在这样的陷阱就要考虑需不需要规避这些陷阱以及该怎么规避这些陷阱。比如在前面的章节里我们讨论了可以使用代理模式当然还有其他的方法。
## 最小限度的授予
权力这东西少了处处掣肘多了飞扬跋扈是一个很难平衡、很难设计的东西。一个操作系统设计有只手遮天的root用户一门编程语言设计有无所不能的AllPermission和特权代码。
这些方式看似可以带来美好的绝对的权力,却恰恰是攻击者喜欢的命门。只要能够获得这绝对的权力,攻击者就可以为所欲为,轻而易举地跨过所有安全防线。只手遮天的权力,从来都是双刃剑!
我们前面讲过权限的三个要素:权限、权限的主体和权限的归属。
```
grant Principal com.sun.security.auth.UnixPrincipal &quot;duke&quot; {
permission java.io.FilePermission &quot;/home/duke&quot;, &quot;read, write&quot;;
};
```
要把这三个要素使用好当然需要花费时间设计好这三个要素并且做好权限的分配。这多多少少有一点点麻烦。于是就有人使用了无所不能的AllPermission。
比如下面例子中的授权策略就授予了my.dirs目录下的所有类库所有的权限。
```
grant codeBase &quot;file:${my.dirs}}/*&quot; {
permission java.security.AllPermission;
};
```
这样的授权策略看着真是痛快、简单。其实,它的复杂性和由此带来的痛苦像是一座隐藏在水面下的冰山。
这个授权要想做到安全至少需要做到两点。第一点就是my.dirs目录受到严格的保护不能放入不被信任的代码。第二点就是my.dirs目录下的代码没有安全漏洞可以泄漏这无所不能的权限。
要想做到第一点,技术本身已经不足以保证,还需要组织管理和规章制度的介入。但是管理和制度的介入,除了让系统维护人员更痛苦之外,还会让安全保障的强度大打折扣。
第二点提到的问题本身就是一个悖论,即使我们有良好的愿望以及强大的实力,也做不到代码没有安全漏洞。所以实际上,这只能是一个永远都不可企及的美好梦想而已。
安全策略的设计和实现,是一个很专业的技术。如果代码有需要,我们需要花点时间学好、用好这样的技术。
### 限制特权代码
类似于操作系统的root用户和安全策略的AllPermission还有一种获取绝对权力的方式那就是使用特权代码。Java中特权代码的调用接口是AccessController.doPrivileged()方法。
AccessController.doPrivileged()获取特权的方法有两种。第一种形式,是使用调用者的权力。如果调用者是一个绝对权力拥有者,这个方法就拥有绝对的权力。
```
public static &lt;T&gt; T doPrivileged(PrivilegedAction&lt;T&gt; action)
```
第二种形式,是在调用者权力许可的范围内,使用指定的权力。这种形式大幅度缩小了特权代码的权限范围,减轻了安全攻击的风险。
```
public static &lt;T&gt; T doPrivileged(PrivilegedAction&lt;T&gt; action,
AccessControlContext context,
Permission... perms)
```
如果你的代码需要使用特权代码,我建议优先考虑使用指定权力的接口。这会让你的代码避免一定的安全风险。
### 特权代码要短小
安全策略的设计和实现,以及特权代码的使用,都是很专业的内容。一般而言,我们应该优先考虑编写和使用无特权要求的代码,这样可以尽量规避掉一些不必要的安全风险和复杂性。
如果不能够避免特权代码的使用,那么特权代码的尺寸一定要短小,只使用它处理需要特权的流程,尽量别在特权代码里处理一般的用户数据和业务。
```
AccessController.doPrivileged((PrivilegedAction&lt;Void&gt;)
() -&gt; {
// Privileged code goes here.
// The code should be short and simple.
// snipped
return null;
}, ...);
```
## 小结
通过对最小授权的原则的讨论,我想和你分享两点个人看法:
<li>
在编码的过程中,要考虑代码的权力;
</li>
<li>
权力的设计和使用,要遵循“优先最小权限”的原则。
</li>
## 一起来动手
代码权力的设计是我们容易忽视的一个问题。即便是熟知的Java修饰符也不是每个人每次都能运用得恰如其分。如果你观察OpenJDK的代码评审可能会发现代码的权力是代码评审者关注的一个重要评审点。恰当运用public、private和final这些修饰符可以有效地提高代码的安全性和可维护性。
这一次的练习题,我们换个角度,来分析下面这段代码中的权力设计问题。
```
import java.util.HashMap;
import java.util.Map;
public class Solution {
/**
* Given an array of integers, return indices of the two numbers
* such that they add up to a specific target.
*/
public int[] twoSum(int[] nums, int target) {
Map&lt;Integer, Integer&gt; map = new HashMap&lt;&gt;();
for (int i = 0; i &lt; nums.length; i++) {
int complement = target - nums[i];
if (map.containsKey(complement)) {
return new int[] { map.get(complement), i };
}
map.put(nums[i], i);
}
throw new IllegalArgumentException(&quot;No two sum solution&quot;);
}
}
```
欢迎你把你的看法写在留言区,我们一起来学习、思考、精进!
如果你觉得这篇文章有所帮助,欢迎点击“请朋友读”,把它分享给你的朋友或者同事。

View File

@@ -0,0 +1,186 @@
<audio id="audio" title="40 | 规范,代码长治久安的基础" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/68/2f/68dcb654139c97c62f9ec36a8a65e92f.mp3"></audio>
如果从安全角度去考察,软件是非常脆弱的。今天还是安全的代码,明天可能就有人发现漏洞。安全攻击的问题,大部分出自信息的不对称性;而维护代码安全之所以难,大部分是因为安全问题是不可预见的。那么,该怎么保持代码的长治久安呢?
## 评审案例
有些函数或者接口可能在我们刚开始写程序的时候就已经接触了解甚至熟知了它们比如说C语言的read()函数、Java语言的InputStream.read()方法。我一点都不怀疑我们熟知这些函数或接口的规范。比如说C语言的read()函数在什么情况下返回值为0 InputStream.read() 方法在什么情况下返回值为-1
我知道我们用错read()的概率很小。但是今天,我要和你讨论一两个不太常见,且非常有趣,的错误的用法。
让我们一起来看几段节选改编的C代码代码中的socket表示网络连接的套接字文件描述符file descriptor。 你能够找到这些代码里潜在的问题吗?
```
#include &lt;stdio.h&gt;
#include &lt;string.h&gt;
#include &lt;unistd.h&gt;
#include &lt;sys/socket.h&gt;
void clientHello(int socket) {
char buffer[1024];
char* hello = &quot;Hello from client!&quot;;
send(socket, hello, strlen(hello), 0);
printf(&quot;Hello message sent\n&quot;);
int n = read(socket, buffer, 1024);
printf(&quot;%s\n&quot;, buffer);
}
```
```
#include &lt;stdio.h&gt;
#include &lt;string.h&gt;
#include &lt;unistd.h&gt;
#include &lt;sys/socket.h&gt;
void serverHello(int socket) {
char buffer[1024];
char* hello = &quot;Hello from server!&quot;;
int n = read(socket, buffer, 1024);
printf(&quot;%s\n&quot;, buffer);
send(socket, hello, strlen(hello), 0);
printf(&quot;Hello message sent\n&quot;);
}
```
```
#include &lt;stdio.h&gt;
#include &lt;string.h&gt;
#include &lt;unistd.h&gt;
#include &lt;sys/socket.h&gt;
void serverHello(int socket) {
char buffer[1024];
char* hello = &quot;Hello from server!&quot;;
int n = read(socket, buffer, 1024);
if (n == 0) {
close(socket);
} else {
printf(&quot;%s\n&quot;, buffer);
send(socket, hello, strlen(hello), 0);
printf(&quot;Hello message sent\n&quot;);
}
}
```
现在我们集中寻找read()函数返回值的使用问题。为了方便你分析我把一个标准的read()函数返回值的规范摘抄如下:
>
<p>RETURN VALUES<br>
If successful, the number of bytes actually read is returned. Upon reading<br>
end-of-file, zero is returned. Otherwise, a -1 is returned and the global<br>
variable errno is set to indicate the error.</p>
上面三段代码里read()函数的返回值使用都有什么问题? 上面的函数能够实现编码者所期望的功能吗?
## 案例分析
上述代码可以作为教学示范的一部分,它们简洁地展示了套接字文件描述符的一些使用方法。但是,这些代码离真正的工业级产品的质量要求还有很大的一段距离。当然了,如果你把上述的代码运行一万次,那么这一万次可能都不会辜负你的期望;运行一百万次,一百万次也可能都是成功的。但是,不论是理论上还是实际上,这些代码还是有可能出现错误的,它们并不是可靠的代码。
问题出在哪儿呢如果我们仔细阅读read()函数返回值规范可以注意到read()函数的返回值是实际读取的字节数。一段信息套接字的底层实现可能会分段传输分段接收。所以read()函数并不能保证一次调用就返回完整的一段信息,传送和接收也未必是一一对应的,即使这段信息很短。
在上述的例子中如果期望接收到的信息是“Hello from server!”那么一次read()函数的执行实际接收到的信息可能是完整的信息也可能是一个开头的字母“H”。套接字的底层实现并不能保证通过调用一次或者两次read()函数,就能够接收到这条完整的信息。
这其实带来了一个不小的麻烦。如果调用read()函数的次数无法确定,那么接收端就要一直读取,直到接收到完整的信息。可是,什么样的信息才是完整的信息呢?接收端似乎并没有办法知道一条信息是否完整。
比如在上面的例子中对于接收端来说怎么知道“H”不是一条完整的信息 “Hello”也不是一条完整的信息而“Hello from server!”就是一条完整的信息呢无法判断信息的完整性就会面临信息丢失或者读取阻塞的问题。所以应用层面的设计必须考虑如何检验接收消息是否完整。比如对于HTTP协议而言请求行必须以“CRLF”结束。那么接收端读取到“CRLF”就能够确定请求行的数据传输完整了。
在实际运行中如果信息足够短比如上面的“Hello from server!”,那么套接字底层的实现和网络环境,大部分情况下都能够一次传输完整的信息。所以,上述代码运行一万次,可能这一万次都是成功的。即便如此,也不能保证每次传输的都是完整的信息。
这里面还有另一个不太小的麻烦是关于read()函数的实现的。函数的规范要求数据传输结束End-Of-Fileread()函数应该返回0。那么read()函数返回0是不是就表示数据传输结束呢 是的。不然的话,应用程序如何判断数据传输结束又是一个大麻烦。
可是的确存在类似的实现读取操作返回了0但是数据传输才刚刚开始。下面我要给你讲的这个例子**就是这样的一个看似微不足道,但后果却很严重的问题,把互联网协议的重要安全变更,耽搁了整整十年**。
## 十年的死局
安全套接字协议( Secure Socket Layer, 简称SSL是用来确保网络通信和事务安全的最常见的标准。现在只要你使用互联网几乎就是这个标准的使用者。这个标准最初由网景公司NetScape设计并且实现后来移交给了国际互联网工程任务组The Internet Engineering Task Force简称 IETF管理并且更名为**传输层安全协议**Transport Layer Security简称TLS
我们通过浏览器输入,并且传输到网站的用户名和密码必须只有我们自己知道,不能在传输的过程中被第三者窃取,也不能传送给指定网站以外的服务器。一般来说,浏览器和服务器之间需要建立安全传输连接。这样,网站的真实性是经过校验的,浏览器和网站之间传输的所有数据都是经过加密的,只有我们自己和网站服务器可以解密、理解传输的数据。
传输层安全协议就是用来满足这些安全需求的。那它是怎么做到呢?传输层安全协议需要使用一系列的密码技术,来保证安全连接的建立。
保证数据的私密性使用的是数据加密技术。其中,影响最大的一类数据加密技术使用的是一种叫作链式加密(Cipher Block ChainingCBC)的模式。简单地说,就是前一个加密数据的最后一个数据块,被用来作为后一个数据块加密的输入参数。这样,就形成了后一个加密数据依赖前一个加密数据的链条。
1999年1月传输层安全协议第一版发布一般简称为TLS 1.0。TLS 1.0使用链式加密模式作为其加密传输数据的一个技术方案。TLS 1.0获得了巨大的成功。我们很难想象如果没有TLS协议互联网会是一个什么样子。然而完美的东西渴求不来也偶遇不到。
2001年9月的密码学进展大会上一位密码学研究者Hugo Krawczyk发表了一篇论文该论文研究了链式加密的缺陷以及对于TLS协议的影响。利用链式加密的缺陷攻击者可以破解出加密密码使用这个密码就可以解密加密的传输数据从而获取传输信息。从此链式加密一个有着最广泛影响的技术开始淡出历史舞台。然而这个进程非常缓慢非常缓慢。在新技术替代的过程中老技术的现有问题以及新老技术的衔接会出现很多非常复杂和棘手的问题。原有的技术使用得越多部署得越广泛这些问题越复杂。
2002年OpenSSL一个被广泛使用的实现传输层安全协议的类库发布了针对链式加密缺陷的安全补丁和缺陷报告。这个解决方案的目的就是打破链式加密模式的链条在数据块之间插入随机数据。由于随机数据插到了加密数据链之间解决了链式加密模式的上述缺陷这使得链式加密的形式和算法得以保留。
幸运的是TLS协议的设计恰巧允许这种使用方法那么TLS协议在理论上仍可以继续使用。既然是随机数据那就是没有任何意义的数据不能用于实际的应用接收端必须忽略这些随机数据。TLS协议通过传输一个空数据段然后再传输有效数据就可以达到添加随机数据的目的。 在理论上,这是一个很好的解决方案。然而,现实比想象的还要精彩。
该解决方案的真正落地需要read()函数或者类似的方法有一个好的实现。在接收到空数据段所代表的随机数据时需要忽略该数据段继续等待真正有效的数据不能返回0。为什么不能返回0呢还记得上面的read()返回值规范吗返回值为0代表数据传输结束应用程序就不应该继续使用该通道了后续的数据都会被丢弃。可是对于这个解决方案如果read()返回0意味着真正的数据传输才刚刚开始而不是结束。
如果这样的实现存在,那么这个解决方案不但没有解决安全缺陷问题,还直接导致应用程序不能继续使用。
有没有这样一时糊涂的实现呢? OpenSSL的缺陷报告里提到了一个这样的糊涂的实现。有这么有一个产品名字的简写是MSIE。曾经它是一种特立独行般的存在到了哪里哪里就会绽放出不一样的烟火。考虑到MSIE及其相关家族产品巨大的市场使用份额谁采用该安全缺陷修复解决方案谁就自绝于市场自绝于广大的用户。遇到了这种巨大的互操作性问题后OpenSSL随后缺省关闭了这个安全漏洞修补方案。随后其他公司比如Google也曾经尝试在他们的产品中做类似的安全修复都因为这种灾难性的互操作性问题而放弃。安全诚可贵自由价更高
对于这样糊涂的实现而言,这只能算是一个芝麻蒜皮的小问题。修复这样的问题也应该不是多么困难的事情。可是,真正的困难在于,这样的产品已经有了非常广泛的用户群体,以及产品部署,包括个人计算机、自动取款机、商超收银机以及银行柜员机等各种各样的形式。
很多产品的部署形式使得产品的升级非常困难,更别提还有很多产品的实现,是以固件的方式存在的了。比如我们家里用的路由器,部署在计算机房里的交换机,以及每辆汽车里的计算机,这些都是升级非常困难的产品。**用户越广泛,部署越广泛,升级就越困难,安全变更面临的挑战就越大。芝麻蒜皮的小问题,都可能构筑困难的障碍,带来巨大的风险,从而造成严重的损失**。
可能你会有疑问,我换一个浏览器不就没事了吗?如果服务器使用的是这样糊涂的实现,那么一个浏览器是没有办法访问这样的服务器提供的服务的。如果这样的服务器被广泛使用,那么一个浏览器的合理策略,就是不开启这种安全缺陷修复。很多网站不能访问的浏览器,是一个不会有人使用的浏览器。
那么,我自己的服务器是不是可以启动这个安全修复呢?问题又回到了客户端,如果客户端使用了这样糊涂的实现,它也没有办法访问修复了的服务器。如果这样的客户端被广泛使用,比如说最流行的浏览器,那么一个服务器的最合理策略就是不开启这种安全修复。假如一个网站有很多用户不能访问,这实在不是网站设计者和拥有者的初衷。
看起来,这似乎是一个死局!
当时的共识是,针对该漏洞的攻击并不会轻易得手,所以即使不修复该漏洞,估计也不是一个多大的问题。同时,针对该漏洞的升级协议也有条不紊地开始了。
2006年经过4年的反复敲打传输层安全协议版本1.1发布一般简称为TLS 1.1。TLS 1.1的一个重要的任务,就是解决链式加密的缺陷。然而,**任何一个标准从制定到落实,都有一段很长的路要走**。TLS 1.1并没有得到业界及时的响应和应有的重视。携带着安全缺陷的TLS 1.0依然统治着传输安全的世界,似乎大家并没有觉得有太多的不妥之处。 时间来到了十年后2011年9月。
## 无奈的少数派
针对链式加密安全漏洞的攻击真的不会轻易得手吗2010年一个年轻人Juliano Rizzo在印度尼西亚的海滩上阅读了OpenSSL的缺陷报告。在优美的印尼海滩上他发现了一种可能非常有效的攻击方法。
2011年9月两位天才般的研究人员Juliano RizzoThai Duong表示给他们几分钟时间他们就可以利用该漏洞入侵你的支付账户。他们给这个攻击技术取了一个超酷的名字BEAST。你要是搜索一下“the BEAST attack”就知道这是一个多么轰动的攻击技术。
他们的研究成果受到了密码学家的高度赞美。但是业界厂商的处境就比较尴尬了。毕竟这是他们十年前尝试修复但是最后不得不放弃修复的漏洞。十年后的今天原来阻碍这个漏洞修复的现实障碍并没有减少。原计划2011年7月份公开发表论文的日期不得不推迟。 因为直到7月份还是没有合适的修复方案。这让人感到有些失望有些沮丧。
7月20日事情有了转机。
如果传输空数据段不被接受那么传输一个字节呢空数据的read()实现可能返回0一个字节的read()实现应该毫无例外地返回1。在TLS 1.0的链式加密模式下传输一个字节时有足够随机的数据插入链式加密数据块之间简单有效地打破链式加密模式的链条。基于这个想法7月20日一个通常被称为1/n-1分割的解决方案被提出并且得到了验证。
由于该方法简单有效主流厂商迅速采纳了这个方案发布了对应产品的安全补丁。幸运的是TLS 1.0续命了十年,业界有更多的时间完成产品的升级换代。不幸但也在预料之中的是,该方案也不是一点兼容性影响都没有。
比如我们案例中讨论的代码就出了大问题。预期收到一条完整的信息“Hello from server!”。 使用了这个安全补丁后就必须要接收被分割的两条信息“H”以及“ello from server!”。如果应用不能处理分割的信息,就不能好好工作了。
幸运的是虽然不能处理分割信息的应用依然存在但是数量很少。而且这是应用自身的问题很难抱怨安全补丁的不是。由于主流的厂商拥抱了1/n-1分割法而存在问题的应用又是少数派这些少数派不得不亲手解决他们自身的问题。否则就面临着应用不得不停工的损失或者承受安全攻击的风险。
对于某一个特定的问题来说,一旦我们成为少数派的一部分,就有可能面临软件安全的风险,以及在兼容性方面做妥协。对于接口规范来说,我们应该严格遵从白名单原则,没有明文规定的行为规范,就不是能依赖的行为规范。
## 小结
通过对这个评审案例的讨论,我想和你分享下面几点个人看法。
<li>
**对于应用接口API的使用一定要严格遵守规范小失误可能造成大麻烦**
</li>
<li>
**对于应用接口API的定义一定要清晰简单描述一定要详实周到。如果使用者对规范的理解感到困难或者困惑可能会带来难以预料的问题**
</li>
<li>
**对于应用接口API的实现一定要在规范许可范围内自由发挥。越是影响广泛的实现越不要逾越规范的界限**
</li>
这是一个特殊的案例,我们好像聊了一个故事。对这个案例,你还有什么看法吗?
## 一起来动手
我们讨论了read()函数返回值的问题可是上述的案例还有其他的问题存在。你还发现了什么问题这些问题该怎么更改你可以使用Java或者你熟悉的语言来修改。这可并不是一个简单的修改我知道你一定会遇到很多问题欢迎留言分享你的修改或者问题。
如果让你给clientHello()或者serverHello()加上规范描述,你会怎么描述?你会用什么样的文字,告诉这个接口的使用者,该怎么正确地使用这个应用接口?这同样不是一个简单的小练习,欢迎分享你的规范描述。
如果你觉得这篇文章有所帮助,欢迎点击“请朋友读”,把它分享给你的朋友或者同事。

View File

@@ -0,0 +1,162 @@
<audio id="audio" title="41 | 预案,代码的主动风险管理" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d7/85/d711ad7736f3e3abe6d19f40de3ae085.mp3"></audio>
上一次,我们聊了保持代码长治久安的基础——代码规范。这一次,我们接着聊第二个方面,代码的风险预案。
有些问题,并没有适用于各种场景的解决办法;有些设计,并不能适用于所有的用户;有些实现,并不能经受过去、现在和未来的检验。在你的日常工作中,有没有这样的情况出现?
做好预案,是我们管理风险的一个很重要的手段。代码的安全管理,也需要预案。
## 评审案例
让我们一起来看一段节选的Java代码变更。
```
public static String[] getDefaultCipherSuites() {
- int ssl_ctx = SSL_CTX_new();
- String[] supportedCiphers = SSL_CTX_get_ciphers(ssl_ctx);
- SSL_CTX_free(ssl_ctx);
- return supportedCiphers;
+ return new String[] {
+ &quot;SSL_RSA_WITH_RC4_128_MD5&quot;,
+ &quot;SSL_RSA_WITH_RC4_128_SHA&quot;,
+ &quot;TLS_RSA_WITH_AES_128_CBC_SHA&quot;,
// snipped
+ };
}
```
对于这段代码我先做一些说明。其中“Cipher Suites”指的是我们在前面一篇文章中提到的TLS协议的密码算法族 “SSL_RSA_WITH_RC4_128_MD5”是一种基于RC4加密技术的算法族“TLS_RSA_WITH_AES_128_CBC_SHA”是一种基于CBCCipher Block Chaining链式加密模式的算法族。
getDefaultCipherSuites()这个方法返回值的顺序就是TLS协议使用这些算法的优先级别。比如变更后的代码“SSL_RSA_WITH_RC4_128_MD5”算法族具有最高的优先级。相应地 “SSL_RSA_WITH_RC4_128_SHA”具有第二优先级。在安全传输的连接中优先级靠前的算法会获得优先考虑。 一旦优先的算法被采用,其他的算法就不会被使用了。
这段代码是Andriod系统的一部分。这个修改发生在2010年5月份这样做是为了使用Android偏爱的RC4加密算法。有了这样的变更Android就能对算法的选择有更好的安排与控制。
想想上一篇文章中我们说到的BEAST攻击这个修改是不是很有前瞻性 BEAST攻击技术是在2011年9月份公布的有缺陷的算法是基于CBC模式的算法。Android提早了一年把涉及问题的CBC模式设为次优选择。Chrome浏览器可能更早做了类似的修改。所以当BEAST攻击技术公开后Google可以很自豪地说“我们很早就优先使用更安全的RC4算法啦。”
可是,这个变更,还是有一点小问题的。
## 案例分析
要想看清楚这个问题,我们还需要讲述一段小插曲。
在1999年设计TLS 1.0的时侯,有两种常用的加密算法类型。一个是**分组加密技术**把原数据分成若干的小块然后一小块一小块地分组加密。3DES是二十世纪九十年代最流行的分组加密算法。另一个是**流加密技术**这种加密方式是把原数据一位一位地运算。RC4是二十世纪九十年代最流行的流加密算法。对这两种算法TLS 1.0都是支持的。其中的分组加密算法TLS 1.0采用的是链式加密模式。
2011年9月25日BEAST攻击技术公开发表。通过上一篇的介绍我们都知道BEAST攻击技术针对的就是链式加密模式链式加密模式不再安全了。你有没有惊喜地发现TLS 1.0的设计真是周到居然还有一个流加密技术可以使用而且RC4算法被广泛支持。这真是一个可以救命的设计。
如果你回看2011年、2012年的安全分析文章很多业界的专家都会推荐使用RC4来替代链式加密模式很多产品也开始变更为优先使用RC4算法。毕竟BEAST攻击是一个不可忽视的安全问题而针对BEAST攻击的补救措施并不是一个完美的解决方案。在业界寻找链式加密模式的替代算法的同时优先使用RC4算法似乎可以让大家喘口气。
这的确是一个救命的设计,但是,这是一个巧合的设计吗? 如果身处1999年我还没有足够的经验来判断这样的设计是有意为之还是仅仅是一个巧合。但是20年后的今天 如果我们的产品只支持一种模式的安全算法,我一定如坐针毡。因为我知道,**短则一两天,长则三五年,一个算法的理论模型或者实现方式几乎一定会被破解**。战战兢兢地等待着这个算法被破解,然后再去寻找补救的措施,显然不是一个可以让工程师心情愉悦、身心放松的好选择。
虽然优先使用RC4可以让业界稍作喘息但是好景并不长。2013年3月13日一个研究小组公开了一个关于RC4算法的严重的安全漏洞。不同寻常的是这一次并没有合适的修改RC4算法的补救措施。该研究小组建议停止使用RC4TLS 1.0和1.1版本的用户应该转化到CBC模式的加密算法。这算是一个不小的玩笑很多应用刚从CBC模式切换到RC4算法不久就要重新调整再切换回去。
这就类似于两个病例。CBC模式虽然是一场大病可是有成熟的救治方案。虽然那里或者这里或许会留个疤可是手术一旦实施成功CBC模式照样活蹦乱跳。 这就好比以前的100米需要跑9.8秒,手术后也可以跑个十一二秒的。虽然离巅峰阶段有点差距,但是问题不算大。
而RC4的问题就像是医生诊断后直接重症监护并时刻准备后事了。冷酷而又无奈2013年3月13日RC4算法宣告重病缠身重症监护。
随后业界开始重新转换回CBC模式很多应用开始禁用RC4算法。2013年8月IETF提出了在TLS协议中禁用RC4算法的议案。2015年2月该议案获得通过。 RC4算法这个因高效、安全而著名的算法从2013年3月开始慢慢淡出人们的视野。
有了上面的小插曲,你知道上面案例代码的问题了吗? **这段代码写死了TLS协议算法的缺省优先级别**。除非更改代码,否则这个缺省优先级别是无法更改的。一旦优先的算法出了问题,代码修改虽然简单,但是已部署产品的升级,有时候就是一件很复杂的事情。
世事无常,**一个好的设计,需要有双引擎和降落伞**。
## 双引擎,长远之计
现代的客机,一般采用双引擎甚至多引擎设计。如果其中一个引擎失灵,依靠其他的引擎依然可以延程飞行。 有人戏称延程飞行是一个“要么多引擎要么去游泳”的设计理念。但是延程飞行时间也是有约束的比如不得超过90分钟。为什么呢 因为延程飞行时,就只有一个发动机在工作了。单引擎运转,总是有更大的安全隐患,这实在是让人不安!
需要注意的是,**双引擎不是备份计划不是应急计划不是Plan B两个引擎日常都要使用**。如果其中一个引擎闲置,那么当真正需要它的时候,我们就不知道它的状态如何,是否可以承担重任。
想一想为什么CBC模式出事的时候业界可以切换到RC4算法 RC4算法出事的时候业界可以切换回CBC模式 其中有很重要的两点值得考虑。
<li>
无论是CBC模式还是RC4算法都是实际投入使用的算法。
</li>
<li>
无论是CBC模式还是RC4算法都是大部分应用同时支持的算法。
</li>
这两条对于CBC模式和RC4算法之间的成功的切换都是必不可少的隐性条件。
如果我们理一理TLS协议发展的脉络就随时可以看到双引擎设计的理念的运用。
1999年TLS 1.0提供了CBC模式和RC4算法两种加密算法。随后2003年发现了CBC模式的安全问题。 2006年发布的TLS 1.1在协议设计层面修复了CBC模式的潜在问题提供了CBC模式和RC4算法两种加密算法。2008年发布的TLS 1.2添加了AEAD加密算法加上已被修复的CBC模式和RC4算法这样就有三种加密算法可供选择。2018年8月发布的TLS 1.3废弃了CBC模式和RC4算法只保留了AEAD算法但是AEAD算法有两个推荐选项分组密码的GCM模式和流密码的Chacha20/Ploy1305模式。到2018年8月TLS协议在这二十年里逐步废弃了二十年前最流行的算法。但是在整个过程中一直保持多算法并存的设计。
如果你熟悉JDK的安全规范和实现可能会注意到对于每一个类型的算法我们总是尽可能地提供多种选择。如果一个算法面临问题我们总是尽快地替换旧算法并且补充新的算法。这样尽快地结束单算法的延程飞行状态。所以提供多种选择不仅仅是为了提升丰富性也是为了在面临关键风险的时候有风险控制的办法。
**对于生死攸关的风险点,我们要有双引擎设计的意识**。 然而,也有双引擎解决不了的问题。即便是多引擎飞机,也需要备用降落伞。
## 降落伞,权宜之计
在上述案例的代码中,算法的缺省优先级别是固定的。 一旦优先的算法出了问题,该怎么办? 如果等到出了问题、蒙受了损失,再去寻找解决方案,就太晚了。一般情况下,一个好的软件应该备好降落伞,提前设计部署好这些意外风险的应急办法。我们永远不希望使用降落伞,但是如果有意外发生,降落伞的存在就非常必要了。随时需要,随时就可以拿来使用。
以JDK为例对于TLS协议的密码算法一旦一个算法出现问题修改源代码替换掉出问题的算法是JDK提供的常规解决方案。另外JDK还提供了多样的应急方案
<li>
修改JVM系统的安全参数Security Property降低出问题算法的优先级
</li>
<li>
修改JVM系统的安全参数Security Property废弃出问题算法
</li>
<li>
修改JVM系统的安全参数Security Property升级到没有问题TLS版本
</li>
<li>
修改应用的系统属性System Property, 使用指定的算法;
</li>
<li>
修改应用的系统属性System Property, 升级到没有问题的TLS版本。
</li>
JVM系统的安全参数可以控制运营在JVM上的所有应用程序而应用的系统属性一般只影响使用它的应用程序。
在JDK中可以通过修改`&lt;java-home&gt;/conf/security/java.security`文件设置JVM系统的安全参数。比如“jdk.tls.legacyAlgorithms”是一个设置TLS历史遗留算法的安全参数。一旦一个算法被设置为历史遗留算法这个算法就不会被优先使用除非不存在其他可替换的算法。如果我们把RC4算法设置为历史遗留算法它的优先级就被降到最低即使它的缺省优先级别是最高的。
```
jdk.tls.legacyAlgorithms = RC4_128
```
一旦在java.security文件中设置了这个参数所有使用这个JDK配置的应用程序都会受到影响。
一个应用程序运行时,可以指定系统属性,比如:
```
$ java -Djdk.tls.client.protocols=&quot;TLSv1.3&quot; myApp
```
那么这个应用程序就使用TLS 1.3版本的客户端。 另外一个运行的程序也可以使用TLS 1.2。 两个运行程序的设置互不影响。
```
$ java -Djdk.tls.client.protocols=&quot;TLSv1.2&quot; myApp
```
通过上面的例子,你可以看到,这些应急方案采用了配置参数的方式,使用非常简单,不需要运营代码的更改。**简单、易用、快速上手,这是我们设计应急降落伞的一个思路。**
一旦一个系统采纳了双引擎和降落伞的设计,系统的可靠性和抗风险能力往往会有大幅度提高。 可是,这并不是白白得来的。它同时也意味着软件研发的巨大投入,和软件复杂度的显著提升。
我们总是尽最大的可能使得软件程序简化、简化再简化。可是对于生死攸关的风险点,我们有时需要选择相反的方向,强化、强化再强化。**不是所有的复杂都是必要的,也不是所有的复杂都是不必要的。软件的设计,是一个需要反复权衡、反复妥协的艺术**。
## 小结
通过对这个评审案例的讨论,我想和你分享下面两点个人看法。
<li>
**尽管我们无法预料未来可能出现的风险,但是软件的设计和实现依然要考虑抗风险的能力**
</li>
<li>
**对于生死攸关的风险点,我们既要有长期的双引擎设计的意识,也要有权宜的应急预案**
</li>
如果深入到软件的架构和设计里,双引擎和降落伞的使用随处可见,你愿意分享你见到过的双引擎和降落伞的案例吗?欢迎在留言区留言。
## 一起来动手
这不算是一个练习,而是一个请求。如果你有时间,你能够研究下你使用的语言、架构或者应用,找找其中的风险防范设计吗? 代码安全和风险控制,是一个需要超大范围合作的技术领域。我们也需要共同创作这一话题,共同学习其中的经验。
比如说,我有个疑问就是,很多业务需要手机验证码,当我的手机不能使用时,我还有没有办法操作我的银行账户?
如果你觉得这篇文章有所帮助,欢迎点击“请朋友读”,把它分享给你的朋友或者同事。

View File

@@ -0,0 +1,122 @@
<audio id="audio" title="42 | 纵深,代码安全的深度防御" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/4b/37/4b7938a5e8737a32f86b9ec88a503337.mp3"></audio>
前面我们聊了保持代码长治久安的两个策略,代码规范和风险预案。这一次,我们接着聊代码安全管理的另外一个策略:纵深防御。
说起纵深防御Defence-in-Depth我们最常想到的是军事战略。在军事上这个概念指的是通过层层设防以全面深入的防御来延迟敌人的进攻通过以空间换时间的方式来挫败敌方的攻击。这有别于一战定胜负的决斗思维。决斗思维需要集中所有的优势资源在最前线一旦前线失守整个战争基本就宣告结束了。
信息安全的攻防,有一个很重要的特点,就是不存在没有漏洞的防线。按照决斗思维部署的信息安全防御体系,也许仅仅只能是个心理安慰。事实上,现代网络安全防御体系和应用架构,不管你是否意识到,已经在广泛使用纵深防御的思想了,或多或少,或明或暗。
## 评审案例
我们一起来看一段OpenJDK的代码修改。其中wrap()方法的传入参数key是一个不能泄露的密钥而key.getEncoded()导出这个密钥的编码,以便进行下一步的加密操作。有时候,密钥的编码可以等同于密钥,也是不能泄露的。你知道这样修改的必要性吗?
```
byte[] wrap(Key key)
throws IllegalBlockSizeException, InvalidKeyException {
byte[] result = null;
+ byte[] encodedKey = null;
try {
- byte[] encodedKey = key.getEncoded();
+ encodedKey = key.getEncoded();
if ((encodedKey == null) || (encodedKey.length == 0)) {
throw new InvalidKeyException(
&quot;Cannot get an encoding of &quot; +
&quot;the key to be wrapped&quot;);
}
result = doFinal(encodedKey, 0, encodedKey.length);
} catch (BadPaddingException e) {
// Should never happen
+ } finally {
+ if (encodedKey != null) {
+ Arrays.fill(encodedKey, (byte)0x00);
+ }
}
return result;
}
```
这个代码变更,是对临时私密缓冲区的更积极的管理。
## 案例分析
我们知道,如果一段存储空间不再使用,一般而言,操作系统或者应用程序仅仅是“忘记”或者“删除”这段存储空间的索引,并不清理存储空间里的具体内容。我们常说“释放”一段内存空间。我觉得“释放”这个词使用很贴切。释放后,那段内存空间还在,模样也没有变化,内容也没有什么变化,只是被释放了,被丢弃了。
上面的例子中encodedKey是一个临时变量定义它的方法调用返回encodedKey就被释放了。在这段代码变更之前encodedKey这段存储空间里的数据在释放前和释放后并没有变化。至于这段空间里的数据什么时候被覆盖则完全依赖于Java和操作系统的内存管理机制以及后续的内存使用。这种不确定性就存在一些隐患。
一段内存空间被释放后,由于这段内存空间的内容还在,这些内容就有可能被未授权的用户拣到、看到。如果这些内容是私密或者敏感的信息,比如密钥、口令、社会保障号码等,那么它们的泄露就是严重的安全事故。
假设还有一个程序,该程序分配了一段内存。那么,这段内存里可能就有别的程序释放、丢弃的内容。这个程序就有可能分析、转存、打印这些内容,进而造成上一个程序的私密信息的泄露。
我们在前面讲过整数的溢出,如果可以远程地制造整数溢出或者其他类型的内存溢出,这段内存空间的信息也可能会被泄露。
实际应用中有很多提高内存使用效率的技术,比如说缓存、虚拟内存、闪存、内存管理技术等等。这些技术在提高效率的同时,也增加了系统的复杂性,加剧了诸如内存溢出这类风险的破坏性。
高效率,是一个让人不懈追求的目标。为了高效率,对于大部分数据的释放,我们可以采取撒手不管的策略;为了安全,对于小部分敏感数据的释放,我们需要采取非常保守的策略。**敏感数据归零**,就是其中的一个保守策略。
敏感数据归零是不是可以绝对避免敏感数据被泄露呢?当然不是,敏感数据归零也有很多解决不了的问题。 比如说对于上面例子中的encodedKey理论上还有以下的风险
<li>
在encodedKey归零之前发生了内存溢出encodedKey有可能包含在被泄露的内存信息中。
</li>
<li>
在encodedKey归零之前底层的内存管理技术拷贝了encodedKey所在的内存块当然也拷贝了encodedKey的内容。这时候encodedKey归零有可能并没有清除拷贝的内容。
</li>
<li>
在encodedKey归零时由于不可控的编译器优化encodedKey的归零操作并没有真正地及时执行。
</li>
看起来真的像筛子一样,到处都是窟窿。那么,敏感数据归零到底还有什么意义呢?敏感数据归零虽然存在这样那样的问题,但是已经显著地降低了我们上面所说的风险。如果没有及时把敏感数据归零,风险会更大。**敏感数据归零是纵深防御体系中,非常具有深度的一个防线,但并不是唯一的防线**。
怎么设计和部署纵深防御体系呢?这是一个无比巨大的话题,我们没有办法几篇文章就交代清楚。下面,我们就尝试来理清楚这背后的逻辑。然后,你可以按照这些逻辑,去寻找相关的技术和实践,把它们运用到你的项目中去。
## 防线,攻击路径的纵深
一个有效的攻击,必须处于一个特定的攻击场景中。对应的防御,就是阻断攻击者到达或者创建这个特定的场景。比如说,上面的例子中,敏感数据的泄露需要攻击者能够访问敏感数据所在的内存。**为了设置纵深防御体系,我们需要在离内存十万八千里的地方就开始布防**。
为了形象一点,我们来看看一个有着“八道防线”的防御场景:第一道防线就是没有人可以接近存放这台计算机的街区,除了居住在街区里的人;第二道防线就是没有人可以接近存放这台计算机的建筑物,除了工作在建筑物里的人;第三道防线就是没有人可以接近存放这台计算机的房间,除了可以在该房间工作的人;第四道防线是没有人可以登录这台计算机,除了该计算机的操作者;第五道防线是没有人可以安装卸载计算机上的程序,除了该计算机的系统管理员;第六道防线是没有人可以访问内存空间,即便是系统管理员;第七道防线是没有人可以查看敏感数据,不管是谁;第八道防线是如果有人查看了敏感数据,或者泄露了敏感数据,隐私保护法会在前面等候。
这么严防死守,敏感数据还有可能被泄露吗? 有可能。遗憾的是,无论是在理论上还是在实践上,还是会有人冲破这些防线。 因为每一道防线都不是完美的每一道防线都有天然的漏洞而且每一道防线都需要执行才能发挥作用。这确实让人不满、不安。甚至有人说这种基于攻击路径的布防已经老掉牙了因为人们发现世界上80%以上的攻击都是由于应用程序的漏洞引起的。可是,要是没有这些防线,实际有效攻击的数量一定会有数量级的爆发性增长。
## 机制,发挥防线的作用
**任何一道防线都不会自动发挥作用,除非我们设置了让防线良性运转的机制**。比如说,针对第一道防线,我们有什么办法让只有居住在该街区的人进入该街区? 谁来守卫这道防线?谁来检查、监管这道防线的有效性?如果非街区的人进入,应该采取什么措施?
还记得我们在[第二篇](https://time.geekbang.org/column/article/76349)文章里,谈到的优秀代码出道前需要经历的重重关卡吗?这些关卡是软件质量保障的一部分,也是让防线机制良性运转的一部分。比如我们上面提到的第七道防线,没有高质量的代码,这道防线的质量就是值得担忧的。
你不妨想想看,对于每一道防线,都应该设置什么样的机制? 每一道防线都考虑**决策、执行、监督这三项权力的分配**,以及**计划、执行、检查、纠正这四项操作的实际执行**。这可能需要花费很长时间,也绝对不是一件容易的事。我相信,把这些问题弄清楚,理明白,哪怕只是针对其中的一道防线,都是一个了不起的成就。
## 多样,加固防线的双保险
最后,再给你介绍一个防护利器,那就是增加防护的多样性。什么是防护的多样性呢?
想一想你的车有没有安装警报器? 如果说车门锁和点火锁是两道防线的话,那么安装警报器就可以增加防线的多样性。警报器是独立于锁的另外一种形式的防护技术。安装有警报器的车不仅试图阻止陌生人使用这辆车(锁),还警告试图使用这辆车的陌生人(警报)。这就是防护的多样性。
比如上面案例中的encodedKey如果代码再长一点我们就需要在最早的时间执行归零操作而不是等待系统的垃圾回收机制发挥作用。这也是多样性的一点点体现它需要时间和空间上的双重考虑。
这里我要特别提醒你,一定要注意**多样性之间的独立性**。我们再来看小区门禁这个例子。一个封闭小区的入口不仅有门禁系统,还有门卫人员。这也算是多样性的防护措施吧。可是,我见到的封闭小区的管理,几乎门卫人员总是可以开启门禁系统。门禁系统和门卫人员并不是完全独立的。有些时候,这不是强化了防线,而是弱化了防线。只要和门卫人员搞好关系,门禁系统就是虚设。在计算机系统中,系统管理员、数据库管理员的权限就有点像门卫这个角色。**如果信息系统防护的多样性之间不能独立,多样性的防护实际上可能会产生多样性的漏洞**。
## 小结
通过对这个评审案例的讨论,我想和你分享下面两点个人看法。
<li>
**没有防御纵深的信息系统,其安全性是堪忧的;**
</li>
<li>
**一个防御体系,需要考虑纵深和多样性,更需要确保防御体系良性运转。**
</li>
## 一起来动手
把纵深防御理念用得最讲究的,在我的见识范围内,我认为是核防护的设计,毕竟这是关乎巨大人群生死存亡的大事。如果要梳理基本概念,树立谨慎保守的防护理念,我建议阅读下核防护的一些规范。 你可以从[核电站通用设计准则](https://www.nrc.gov/reading-rm/doc-collections/cfr/part050/part050-appa.html)开始。
那么怎么把理念落实到应用软件呢我建议你阅读Oracle的[纵深防御指南](https://www.oracle.com/technetwork/database/security/sol-home-086269.html)。这份指南虽然是为Oracle数据库准备的文档但是其中涉及的很多思想、方法和技术同样适用于其他的软件和编码领域。非常难得的是这个纵深防御指南罗列了不同维度的很多检查点。我们可以使用这些检查点来查验我们的软件设计和实现。
聊起纵深防御这个话题,就真的不是几篇文章可以说的完的。希望今天的内容能够抛砖引玉,欢迎你把自己的经验和看法写在留言区,我们一起来学习、思考、精进!
如果你觉得这篇文章有所帮助,欢迎点击“请朋友读”,把它分享给你的朋友或者同事。

View File

@@ -0,0 +1,236 @@
<audio id="audio" title="43 | 编写安全代码的最佳实践清单" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/0b/88/0b64312442f1a8ed16450eddec802b88.mp3"></audio>
像以前一样,当大家看到“最佳实践清单”这个标题的时候,就意味着这一个模块又到了总结的时候了。
这一模块我们从代码安全的角度出发,探讨了如何编写安全的代码。首先我们再来重温一下,为什么需要安全的代码呢?
## 为什么需要安全的代码?
1.代码质量是信息安全的基础
大部分的信息安全事故,是由软件代码的安全缺陷引起的。没有安全质量保证的代码,建立不起有效、可信的信息系统。信息系统的安全,主要依赖的不是信息安全技术专家,而是我们每一个编写代码的工程师。
2.安全漏洞的破坏性难以预料
直到真实的安全问题发生之前,我们都难以预料软件的安全漏洞到底有多大的破坏性。一个小小的安全漏洞,如果被攻击者抓住了时机,就可以瞬间摧毁多年的苦心经营和良好声誉,把公司推到舆论的风口浪尖,甚至使公司面临毁灭性的风险和挑战。
3.安全编码的规则可以学得到
由于安全攻击技术的快速发展,安全编码涉及到的细节纷繁复杂,安全问题的解决甚至需要大规模、大范围的协作。编写安全的代码不是一件轻而易举的事情。但是,安全编码的规则和经验,却是可以学习和积累的。使用必要的安全管理工具,开展代码评审和交流,也可以加速我们的学习和积累,减少编写代码的安全漏洞。
要想掌握安全编码的技术,熟练修复软件漏洞的实践,我们需要跨过意识、知晓、看到三道关卡。面对最新的攻击技术和安全问题,通过每一道关卡都障碍重重。我们要主动地跟踪安全问题的最新进展,学习最新的安全防护技术。
及时更新自己的知识,掌握难以学习到的知识和技能,也是构建和保持我们竞争力的一个重要办法。
## 编写安全代码的基本原则
1.清楚调用接口的行为
使用不恰当的接口,是代码安全风险的主要来源之一。我们一定要了解、掌握每一个调用接口的行为规范,然后在接口规范许可的范围内使用它们。不要去猜测接口的行为方式,没有明文规定的行为,都是不可靠、不可信的行为。
2.跨界的数据不可信任
跨界的数据面临两大问题:一个问题是数据发送是否可信?另一个问题是数据传递过程是否可靠?这两个有任何一个问题不能解决,跨界的数据都可能被攻击者利用。因此使用跨界的数据之前,要进行校验。
3.最小授权的原则
信息和资源,尤其是敏感数据,需经授权,方可使用。所授予的权力,能够让应用程序完成对应的任务就行,不要授予多余的权力。
4.减小安全攻击面
减小、简化公开接口,缩小可以被攻击者利用的攻击界面。比如,设计更简单直观的公开接口,使用加密的数据传输通道,只对授权用户开放服务等等,这些措施,都可以减少安全攻击面。
5.深度防御的原则
使用纵深防御体系防范安全威胁。要提供深度的防御能力,不能仅仅依靠边界的安全。编写代码,要采用谨慎保守的原则,要解决疑似可能出现的安全问题,要校验来源不确定的数据,要记录不规范的行为,要提供安全的应急预案。
## 安全代码的检查清单
**安全管理**
<li>
有没有安全更新的策略和落实计划?
</li>
<li>
有没有安全漏洞的保密共识和规范?
</li>
<li>
有没有安全缺陷的评估和管理办法?
</li>
<li>
软件是不是使用最新的安全修复版?
</li>
<li>
有没有定义、归类和保护敏感信息?
</li>
<li>
有没有部署多层次的安全防御体系?
</li>
<li>
安全防御能不能运转良好、及时反应?
</li>
<li>
不同的安全防御机制能不能独立运转?
</li>
<li>
系统管理、运营人员的授权是否恰当?
</li>
<li>
有没有风险管理的预案和长短期措施?
</li>
**代码评审**
<li>
数值运算会不会溢出?
</li>
<li>
有没有检查数值的合理范围?
</li>
<li>
类、接口的设计,能不能不使用可变量?
</li>
<li>
一个类支持的是深拷贝还是浅拷贝?
</li>
<li>
一个接口的实现,有没有拷贝可变的传入参数?
</li>
<li>
一个接口的实现,可变的返回值有没有竞态危害?
</li>
<li>
接口的使用有没有严格遵守接口规范?
</li>
<li>
哪些信息是敏感信息?
</li>
<li>
谁有权限获取相应的敏感信息?
</li>
<li>
有没有定义敏感信息的授权方案?
</li>
<li>
授予的权限还能不能更少?
</li>
<li>
特权代码能不能更短小、更简单?
</li>
<li>
异常信息里有没有敏感信息?
</li>
<li>
应用日志里有没有敏感信息?
</li>
<li>
对象序列化有没有排除敏感信息?
</li>
<li>
高度敏感信息的存储有没有特殊处理?
</li>
<li>
敏感信息的使用有没有及时清零?
</li>
<li>
一个类有没有真实的可扩展需求能不能使用final修饰符
</li>
<li>
一个变量能不能对象构造时就完成赋值能不能使用final修饰符
</li>
<li>
一个方法子类有没有重写的必要性能不能使用final修饰符
</li>
<li>
一个集合形式的变量,是不是可以使用不可修改的集合?
</li>
<li>
一个方法的返回值,能不能使用不可修改的变量?
</li>
<li>
类、方法、变量能不能使用private修饰符
</li>
<li>
类库有没有使用模块化技术?
</li>
<li>
模块设计能不能分割内部实现和外部接口?
</li>
<li>
有没有定义清楚内部数据、外部数据的边界?
</li>
<li>
外部数据,有没有尽早地完成校验?
</li>
<li>
有没有标示清楚外部数据的校验点?
</li>
<li>
能不能跟踪未校验外部数据的传送路径?
</li>
<li>
有没有遗漏的未校验外部数据?
</li>
<li>
公开接口的输入,有没有考虑数据的有效性?
</li>
<li>
公开接口的可变化输出,接口内部行为有没有影响?
</li>
<li>
有没有完成无法识别来源的数据的校验?
</li>
<li>
能不能不使用序列化技术?
</li>
<li>
序列化的使用场景,有没有足够的安全保障?
</li>
<li>
软件还存在什么样风险?
</li>
<li>
有没有记录潜在的风险问题?
</li>
<li>
有没有消除潜在风险的长期预案?
</li>
<li>
有没有消除潜在风险的短期措施?
</li>
<li>
潜在的风险问题如果出现,能不能快速地诊断、定位、修复?
</li>
## 小结
学会编写安全的代码,是一个优秀的、专业的软件工程师的核心竞争力之一。与规范、经济的代码相比,安全的代码有很多不同的特点。
代码不规范和效率不高,业务也可以运转,然后慢慢优化,逐渐演进。但代码一旦出现安全问题,遭受攻击,损失立即就会反映出来,而且破坏性极大。
代码不规范,看的人立刻就会觉得很难受。代码的效率不高,业务运转不通畅,同样会有及时的反馈。就代码的安全层面来说,一般情况下直到攻击发生之前,我们可能都不知道代码是否存在安全问题。等到攻击真实发生的时候,损失已经成为事实了。
代码的规范原则,是一个相对容易掌握的内容。高效的代码,也有很多成熟的经验可以学习。可是,代码的安全,却是一个攻易守难的问题。哪怕我们今天知道了所有的攻击和防护方法(这当然不可能),如果明天出现了一种新的攻击手段,而且全世界只有一个人知道,我们的系统都存在潜在的安全威胁。
编写安全的代码,需要掌握复杂的知识,而且需要大规模的合作。我们之前提到过三道槛,具体展开来是这样的:
>
<p>我们要想掌握安全编码的技术,熟练修复软件漏洞的实践,需要先过三道关。<br>
第一道关是意识Conscious。也就是说要意识到安全问题的重要性以及意识到有哪些潜在的安全威胁。<br>
第二道关是知晓Awareness。要知道软件有没有安全问题安全问题有多严重。<br>
第三道关是看到Visible。要了解是什么样的问题导致了安全漏洞该怎么修复安全漏洞。</p>
在意识、知晓、看到这三道关面前,我们要打开自己的视野,保持强烈的好奇心,从全世界范围内学习成熟的经验、先进的技术以及最新的进展。
其中最重要的资源是NIST提供的[安全漏洞数据库](https://nvd.nist.gov/)。这个数据库的使用方式有两种:第一种是了解自己的系统有没有最新的安全漏洞;第二种是学习最新的安全威胁的攻击方法和防范技术。
## 一起来动手
我们今天的练手题就学着使用NIST的[安全漏洞数据库](https://nvd.nist.gov/)。请你从这个数据库里,选择一个或者几个安全漏洞,试着看一下你的系统有没有类似的安全威胁?这个安全漏洞的攻击方式是什么样的?这个安全漏洞的问题出现在哪里?该怎么防范?
欢迎你在留言区留言、讨论,分享你的阅读体验。
如果你觉得这篇文章有所帮助,欢迎点击“请朋友读”,把它分享给你的朋友或者同事。

View File

@@ -0,0 +1,89 @@
<audio id="audio" title="44 | “代码安全篇”答疑汇总" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/c1/13/c1374c7b010cab596c0b2a4bf59ffb13.mp3"></audio>
到这一篇文章,意味着专栏第三模块“安全的代码”也更新完毕了。今天,我来集中解答一下留言区里的一些疑问。
@醉侠
>
希望老师后面能多讲讲安全编码的例子或者推荐好的书籍,这块儿确实是很大的弱点。
不同于其他的编码技术编码安全是一个在攻与防的拉锯战中持续发展的领域。新的攻击技术花样翻新防守技术也跟着变化。因此编码安全的技术和技巧也纷繁复杂。CWE的常见安全问题列表到目前为止已经列举了1131种问题。其中每一个问题都会给软件带来难以预料的安全风险。由于安全问题和相应的技巧数量如此巨大学习几种或者十几种编码安全的技巧能起到的作用并不是很大。
但坐以待毙显然不是我们的风格。安全问题的数量和技巧虽然庞大,但是基本的原理数量并不是很多,而且都很直观。比如,只要你记住,“跨界的数据不可信任”这条简单的原理,并且去校验每一个跨界数据的有效性,就消除了代码的很大一部分风险。至于什么是有效的数据,什么是有害的数据,每一个场景都有不同的定义。当你想要践行这条原理的时候,你总能定义数据的有效性,找到检查的办法。记住了这一条原理,面对跨界数据的时候,你就会加倍警觉,跟踪数据检查点,想办法校验数据。
所以,我能给的第一个建议就是:**记住最基本的安全编码原理**。
遗憾的是,我们掌握每一样技术的过程,都是先有量变,才会有质变。就比如说,“跨界的数据不可信任”这条原理,想要把它变成我们编码时的下意识行为,就需要很多的锻炼。刚开始的时候,要去学一些技巧,然后照葫芦画瓢。慢慢地,不需要葫芦,自己也能画好瓢了。再慢慢地,你想画啥画啥,不管是瓢还是壶。到最后,引领你的思路的,就是基本的原理,而不再是纷繁复杂的技巧。你也就从按照技巧来编码的阶段,转变到根据问题和基本的原理,去寻找技巧,解决问题的新阶段。
比方说吧你知道了一个数据校验的技巧整数不能太大。于是遇到跨界的整数数据你都会检查这个整数是不是太大。这就是一个很好的实践。如果遇到浮点数呢如果遇到用户密码呢如果遇到SQL查询语句呢你会不会想到浮点数不能太大用户密码不能太长SQL查询语句也不能太长遇到不同的场景不同的数据这时候最能给你提供帮助的就是记住“跨界的数据不可信任”这条原理。然后去想办法去检验整数、浮点数、用户密码、SQL语句。
从哪里开始积累这个“量变”的量呢?我能给的第二个建议是:**学习编程语言的编码规范以及安全编码指南**。
一门编码语言的规范,通常会告诉大家编码的最佳实践。这些最佳实践,就包括安全编码的内容。比如[C](https://resources.sei.cmu.edu/downloads/secure-coding/assets/sei-cert-c-coding-standard-2016-v01.pdf)/[C++](https://resources.sei.cmu.edu/downloads/secure-coding/assets/sei-cert-cpp-coding-standard-2016-v01.pdf)语言的编码规范里,就有如何处理整数、浮点数、内存的很多技术和技巧。
如果你学习的是Java[Java的安全编码指南](https://www.oracle.com/technetwork/java/seccodeguide-139067.html)是一定要掌握的内容。Java的安全编码指南是Java安全组对潜在的Java编码安全问题的总结既有原则又有示例。无论是设计还是实现我们都要把安全编码指南考虑进来。
Java语言已经有二十多年的历史了每更新一个版本它的安全编码指南都会加入新的内容。这也是安全攻防发展的表现之一。每隔一段时间总会有新的攻击技术出现总会有新的安全编码实践。所以每一个JDK版本推出的时候我们还要检查一下新版的安全编码指南看看有没有新加的内容。然后看看我们的代码有没有需要调整的地方。
一般情况下,每一门语言都会有编码规范和安全指南。其中,一个常用的资源是卡内基梅隆大学的[SEI CERT系列](https://wiki.sei.cmu.edu/confluence/display/seccode/SEI+CERT+Coding+Standards)的总结。你自己也找找看,有没有你最喜欢、最适合的资源。
如果你掌握了编码规范和安全编码指南,那就是一个了不起的成就了。但是,对于一个你编写的软件的安全性而言,这还不够。因为,安全的攻防技术是一个持续发展的技术。今天还是安全的系统,明天也许就不安全了。所以,我们也要跟得上变化。
因此,我能给的第三个建议是:**跟踪、学习、使用最新的安全编码进展**。
由于安全漏洞的保密性要求,关于安全编码的最新技术的资源相对来说是稀缺的。一般来说,安全补丁出来的时候,我们大部分人知道的就是:安全补丁出来了,系统需要升级,运维有的忙了。对于研发人员来说,最重要的信息其实是安全补丁补了啥?安全问题在哪儿?是怎么修复的?我们的系统有没有类似的问题?不是所有的安全补丁都会披露安全问题的细节,所以有时候,我们需要去看源代码,去推测到底有什么安全问题,去推测为什么补丁会起作用。
但是这样做太花费时间,幸运的是,有很多人这样做,并且分享了他们的研究成果。我们要做的就是,根据安全问题,使用搜索引擎,搜索相关的研究,然后分析、检查、使用到我们自己的代码里。如果没有现成的研究成果,我们就需要自己动手分析这些安全补丁。
知晓安全问题存在的最重要资源是NIST的[安全漏洞数据库](https://nvd.nist.gov/),我建议你一定要用好这个数据库。
我知道这并不容易。我们都期望的模式是,培训几个小时,然后天下行走。一个好的程序员是时间堆积起来的。**只有持续地了解、积累、训练,才能慢慢地到达一个期望的水准,才能建立、巩固自己的技术优势。**
我在学习金融课程的时候,经常被各种复杂的数字设计弄得迷迷糊糊。老师最常用的鼓励的话就是:**要学习一点难的东西,这样才能走到更远的地方**。我把这句话也送给你。
@轻歌赋
>
<p>有个问题案例中hashtable增加了一个entryset后攻击者如何直接访问对象的entryset呢<br>
以web程序为例的话我想不出用户如何传入可以执行的代码能过直接让权限检查的调用对象直接执行entryset也看不出对方如何能够重写我服务端的代码或者继承并且被jvm加载。<br>
老师能给个实际的例子吗?</p>
@hua168
>
如果是web程序的话攻击者是怎么查看我们内部程序 如果是API接口的话这些方法我们不是隐藏起来不公开它怎么绕过漏洞攻击
答:上面的这两个问题,是一个比较典型的容易迷惑的地方。要想了解这个问题,我们还需要了解我们曾经讲到的边界问题,以及公开接口本身的问题。
比如Web服务吧用户通过浏览器访问Web服务传输使用的HTTP或者HTTPS协议并没有机会直接获取服务器端提供服务的Java对象。这样的话服务器端提供应用服务的Java对象只是系统的一个内部实现外部接口其实是在HTTP层。这就是一个很好的边界隔离。
公开接口的问题在于我们并不知道一个公开接口到底会在什么环境下使用也许不是Web环境。我们也不知道使用公开接口的代码是不是恶意代码也许它是不可信任的代码。公开接口要做的就是不管调用者有什么意愿接口的实现都按照规范执行调用者不能改变接口的规范。要不然就是一大堆的问题。
就拿VM虚拟机来说吧一个VM上可能运行了多个用户每个用户之间并不相互信任而且虚拟机也不会完全信任每个用户可能运行多个应用每个应用之间也不相互信任而且虚拟机也不会完全信任每个应用。这些用户、应用为什么能够运行在虚拟机上呢唯一的办法就是调用虚拟机提供的接口来获得虚拟机的资源。而虚拟机要把用户隔离开来把应用隔离开来就需要严格的权限安排和调度。而要想实现这些权限的安排和调度同样需要通过接口来实现。如果可以越过虚拟机的权限管理虚拟机上的用户和应用就会面临巨大的安全威胁。
虚拟机及其权限管理的思想不仅仅只是应用在JVM或者云计算。像我们日常使用的浏览器Firefox、Chrome、服务器(nginx、Apache HTTP Server)、打印机,都需要考虑多租户、多任务的问题。
@hua168
>
像我们开发是直接调用框架函数,如果是安全问题,一般是框架自身的问题吧?
这是一个很好的问题。我们的系统每次都及时升级到最新的安全修复版,是不是就够了呢?
如果所有的代码都能够做到及时地推出安全修复版,包括你自己的代码,这样做就够了。
但是,我们常常遗忘了一点,像框架的代码一样,我们自己写的代码也是代码,也会存在安全问题。成熟框架的安全问题虽然很少,但是出现的安全问题通常引人瞩目。我们自己写的代码,存在的安全问题可能会更多,但是通常没人关注,直到问题突发。
如果你留意一下NIST的安全漏洞数据库在一段时间内最新的安全漏洞你可能会发现出现问题的代码和应用千奇百怪涉及的领域和技术非常广泛不仅仅局限于框架、语言、开源代码。有代码的地方就有安全问题。
我们勤奋地给系统打补丁,但是很少去审视自己编写的代码是不是存在新的安全问题,很少主动地去修复自己代码里的安全问题。这不是你我的个人问题,这是公司的管理和投入问题。有质量的软件维护是昂贵的。
以上就是这次答疑的内容。如果你还有没有解决的疑问,请在留言区给我留言。
如果你觉得这篇文章解决了你的疑惑,对你有所帮助,欢迎点击“请朋友读”,把这篇文章分享给你的朋友或者同事。