“9月25日周二下午,我们的工程团队发现了一个影响近5000万个账户的安全问题,”Facebook的Guy Rosen在公司9月份的一次安全更新中表示到。
这个问题很严重。被盗的并不是密码,而是访问令牌(access token),这是一个不透明的字符串,用于标识用户,并授予对API的访问权,这里的API指的是应用程序用户访问Facebook时使用的软件接口。
获得令牌后,攻击者便可以以其他用户的身份登录Facebook,更糟糕的是,这些不法分子还可以使用偷来的Facebook用户身份来验证登录其他网站,不过目前尚不足清楚这种情况是否已经发生,攻击背后的幕后主使也不得而知。
这个安全灾难的原因是一个编程错误,或者更确切地说,是工程VP Pedro Canahuati 所述的,这是“三个bug的组合”。第一个是发生在视频只读API中的bug,第二个是视频上传API中的一个bug,后者生成一个具有广泛权限(Facebook移动应用程序权限)的访问令牌。而第三个(可能也是最严重的)bug是生成的访问令牌针对的不是当前用户,而是正在查看概要文件的另一个用户。
该事件表明,bug永远无法避免,即使是那些资源最丰富的软件项目,而且人们也很难发现那些不会立即影响功能的bug。虽然测试是今天软件开发和部署中不可或缺的一步,但是没有哪个测试可以试验所有可能的输入组合,或者展现出所有可能的失败。
这个案例还展示了当今软件bug的影响是如何被某些代码的大量使用和放大的。在本例中,我们讨论的是攻击者可能会利用大约5000万个帐户,来访问未知数量的第三方站点。
另一个近期的例子是: 2018年10月,微软在没有充分测试的情况下发布了Windows 10的特性更新,而其中的一个bug会在某些情况下会删除用户数据。在被下架之前,该更新已经被安装到了数千名用户的系统中,微软为此付出的声誉、补救和支持成本可想而知。
时间往前推移一些,还有2014年发现的“心血”(Heartbleed)漏洞, 它源自OpenSSL加密库中的一行代码:
memcpy(bp, pl, payload);
该代码不会检查数据的实际大小是否与复制的数量相匹配。因此,攻击者可以在大小上进行欺骗,并接收出现在内存中的其他数据副本,其可能包括用户名和密码。这个bug影响了Apache和Nginx web服务器,而这些服务器又被世界上三分之二的活跃网络站点以及无数其他网络应用程序使用着。
向前一步,退后两步
2011年,风险投资家Marc Andreessen因认为软件正在吃掉世界而登上新闻头条。某种程度上,这个表述还算正确,但是我们可能都没注意到软件中的bug问题。早在二十年前,程序员及作家Steve McConnell 就认为按行业平均水平,每1000行的代码中就会存在15到50个bug,同时权威IT项目跟踪机构Standish Group也在报告中指出,1995年有三分之一的软件项目最终取消,这带来了810亿美元的浪费。McConnell和Standish Group简直是软件交付派对上的幽灵,他们进一步加剧了软件交付实践质量不佳的坏名声。软件项目总是会延期,而且总是会存在大量的bug。在那时,如果真的有软件项目真的能按时、按预期并按预期运行,那真可以用惊喜形容。
不过,那是上世纪90年代的旧事了,如今的软件工具、开发实践和文化自那时起已发生了很大变化。这类转变的突出代表包括测试驱动型开发和源于极限编程方法论(Extreme Programming methodology)的代码覆盖概念(即测试期间执行代码的数量)。静态代码分析是另一个突破,该方法使用工具执行编码标准,并捕捉比较明显的bug。同时,编码标准本身也在不断发展,包含了使代码变得更可靠的知识,例如保持代码单元简短。
更安全的编程语言也在提供帮助。具有自动内存管理且不直接使用指针的高级语言,如1996年首次发布的Java,它使开发人员更容易避免一些错误。在最近,像GitHub这样的云托管工具的出现,也使得各个团队的中软件生命周期管理工具变得更加易用。
开发人员是否因此保持了代码的“清洁”?
随着软件变得越来越普遍,答案应该是肯定的。软件不再仅仅是工作场所(Windows个人电脑和服务器)的专利,也不再是为晦涩难懂的工业控制系统编写的复杂流程。但同时软件缺陷带来的风险,也不再是仅存在于公司的服务器中了。
据2017年Mitre常见缺陷列表(CWE)所示,实际情况并不乐观。首先就是注入攻击(injection attack),它包括SQL注入在内,在此情况众,用户输入可以执行未经授权的命令。 多年来,该缺陷一直为人所知,并不断引发问题。
CWE列表中收录的另一大缺陷是“使用具有已知漏洞的组件”。软件开发人员一直依赖于代码库;而任何项目中的大多数代码都是由其他人编写的。但是,正如Heartbleed所显示的那样,库和组件并不能避免漏洞。当库或组件中的错误被发现时,它将被修复。
不过,修补所有正在使用的应用程序中的bug是非常具有挑战性的。许多受到影响的应用程序可能得不到良好地开发,一些bug还可能存在于永远得不到修补的网络设备固件中。因而物联网变成了“bug网”,除非有自动补丁存在,同时供应商也提供细致的补丁管理。
此外,bug不仅很昂贵,而且还性命攸关。卡耐基梅隆大学教授Phil Koopman专门研究嵌入式软件的质量,比如自动驾驶汽车和其他类汽车软件安全等,在最近的一篇关于汽车软件缺陷与潜在致命性的文章中,Koopman列出了50个令人不安的缺陷报告,如突然加速、无法脱离自动驾驶控制以及妨碍司机进行转向控制等。
降低代码复杂度
Koopman指出,提高软件质量在很大程度上就是观察最佳实践。
这包括降低代码复杂性、使用静态分析工具和零警告编译、认真检查实时代码调度、严格软件测试以及使用基本工具(包括配置管理、版本控制和bug跟踪等)。
CAST Research Labs在2017年发表了一篇关于应用软件健康程度的报告,该报告基于遍及8个国家和329个组织的1850个“大型、多层、多语言的商用程序”,一共具有10亿行代码。该报告基于五个健康因素:稳定性、安全性、效率、可更改性(代码修改难度)和可转移性(对于刚接触代码的开发人员来说理解代码有多难)。
CAST的报告令人鼓舞,所有类别的总体平均得分在3.0或以上,其中安全性最好在3.22分,可移植性最差在3.0分。这种相当高的质量水平反映了这样一个事实,即这些应用程序通常会在资源丰富的部门中起关键作用。
但这些结论仍值得再推敲一下。比如安全评分差异较大,这说明缺乏安全编码实践仍然是一个问题。在团队规模方面,CAST认为超过20名开发人员的团队得分较低,并建议最佳团队规模应该在10人或更少。
另一个有趣的地方是,最佳评分项目背后的方法既不是敏捷性的(强调迭代开发),也不是瀑布式的(强调预先计划),而是一种混合方法,它具有广泛的预先分析,然后是简短的迭代编码冲刺。
CAST还指出,涉及“跨应用程序的多层多组件”的软件架构类的代码质量更难测试;但结构质量才是导致大多数运营问题的原因。
慢编码
编写防弹代码的速度依然较慢,因此开发时成本较高,这也是软件质量仍然如此参齐不齐的一个原因。众所周知,缺陷发现得越晚,修复缺陷的成本就会越高,不过,由于差异很大,很难给出缺陷造成具体多大影响的通用数字。
在web应用程序的DevOps流程中,我们可以进行代码更改、自动测试并快速部署到生产环境中,所以修复最近发现的bug的成本可能不算太高。但这里有个极端的例子,1996年阿丽亚娜5号火箭(Ariane 5) 在发射时发生了爆炸,这是由64位变量转换为16位变量后数量太大导致系统无法容纳引起的,这个bug的直接经济损失约为5亿美元。
而日常生活中呢?根据IT软件质量联盟(Consortium for IT Software Quality)的数据,除去技术债务后,所谓的“低质量”软件给美国经济造成2.26万亿美元的损失。再细分一下,软件故障造成的损失占该数字的37.46%,查找和修复缺陷的任务占16.87%。
对于任何公司来说,在生产过程之前,找到bug并进行修复是一个优先事项,因为修复bug或处理后续问题所涉及的成本会随着时长的延长而增加。
因此,生产过程中的bug可能会带来严重的长期成本。如果bug出现在其他软件所依赖的软件或固件中,例如API,那么第三方开发人员可能必须要围绕该bug编写代码。然后这就会出现兼容性问题,因为修复这个bug可能会破坏其他软件。
当今互联时代与早期软件开发时代的一个关键区别是,部署补丁很容易,而且通常是自动化的。由于使用了一些技术,例如提示用户将崩溃数据提交回开发人员,或者使用“飞行记录器”代理捕获应用程序状态并准确记录崩溃时正在执行的代码,现在人们更容易跟踪导致崩溃的bug。通过快速发现和修复,可以减轻明显的缺陷。
在McConnell的言论发表了几十年后,尽管经历了许多变化,但我们遇到的软件质量挫败仍然存在:编写可靠代码所需的是知识和工具,但包括财务,截止日期,错误管理,技能短缺和处理遗留代码与系统的人为因素仍意味着代码质量的不均衡。
考虑到软件的巨大和日益增长的重要性,bug的持续肆虐既发人深省又令人不安。实现最小化部署补丁的负担的系统当然是有帮助的,但是从源头上提高软件质量才是最关键的。
来源:The Register
作者:Tim Anderson
编译:张飞逸