Java 平台 JDK 1.0 于 1995 年发布,采用了一种简单化的非黑即白的“沙箱”安全模型。李恭于 1996 年加入 Sun Microsystems 的 JavaSoft 部门,并领导了安全架构的重新设计,该架构于 1998 年首次在 JDK 1.2 中发布,如今已部署在众多系统和设备上,不仅在桌面端,还在企业和移动版本的 Java 上。
本文回顾了从设计和工程角度来看的一些最艰难的技术问题,以及研究科学家很少接受培训的一些棘手的商业挑战。李恭在此回顾了他在之前四个场合挖掘旧笔记并刷新记忆的经历:2002 年信息安全经济学研讨会,加利福尼亚州伯克利;2003 年 UW/MSR/CMU 夏季学院,华盛顿州史蒂文森;2009 年 ACSAC(年度计算机安全应用会议),夏威夷州檀香山;以及最近,2011 年 5 月,在英格兰剑桥大学计算机实验室的研讨会上。
尽管安全架构师不是“商人”,但他们必须清楚自己的客户是谁,这一点很重要。他们很少直接为个人最终用户构建,尽管最终用户通常是最终受益者,但最终用户并不直接使用操作系统。
安全架构师的大部分工作都针对应用程序程序员,Java 也不例外。此处的设计目标是帮助程序员从他们的代码中获得预期的结果——更具体地说,是使最常见的情况最容易编写和正确,并降低编码错误或漏洞的风险。因此,Java 安全架构1的四个属性应普遍适用
* 可用性。为了在市场上普及和被接受,该架构必须易于使用,并适用于编写各种应用程序。
* 简洁性。为了激发对架构正确性的信心,必须易于理解关键的设计属性并分析实现。
* 充分性。该架构必须包含所有基本功能和构建块,以支持更高级别的安全要求。
* 适应性。设计必须易于演变,以适应需求和市场现实。特别是,它应避免过度规定限制可编程性。
事后看来,拥有这些指导原则至关重要。在最初的 JDK 1.0 中,安全机制完全是关于特殊情况——代码在沙箱内部还是外部。这种看似简单的架构反常地导致了复杂的设计、脆弱的代码和大量的安全漏洞。在 JDK 1.2 中,安全被设计为通用、系统化和简单化,这导致了一个更健壮和可用的平台。我们击退了来自 Netscape 的竞争设计,该设计专门用于浏览器使用。我们的设计不仅范围广泛,涵盖桌面、服务器以及嵌入式和移动设备,而且足够具体,使程序员能够构建以浏览器为中心的应用程序。我们将在本文后面回到这些主题。
设计愿望是确保 Java 代码按预期执行,而不会产生不良的副作用。这个目标有三个组成部分。第一个是确保只接受有效的 Java 代码;这是当前章节的主题。第二个是确保按设计发生预期的行为;这通常通过测试来解决,并且已被很好地理解,因此在此不再赘述。第三个是防止不良的意外行为,例如访问本不应被允许的敏感数据;这将在稍后关于最小特权原则的章节中处理。
另一个通常是隐含的要求是,所有检查和平衡都必须以合理的速度完成——这意味着系统的性能特征与根本没有安全机制的系统相当。这里的威胁模型主要关注可能从事恶意行为的不受信任的代码。保护机制旨在阻止这些恶意行为;它还有助于降低良性编码错误的风险,尽管它不能期望防止所有错误的编程实践,例如不验证可能导致 SQL 注入攻击的查询。
通常,应用程序用 Java 源代码编写,编译成平台无关的 Java 字节码,然后由 JVM(Java 虚拟机)执行。可以将 Java 源代码直接编译为特定于机器的本地代码(例如,用于 x86 系统)。这里不再进一步讨论这种情况,因为编译后的本地代码绕过了 Java 机制,并且无法完全在 Java 平台内处理,除非作为允许或禁止本地代码访问和执行的一种方式。也可以直接编写 Java 字节码,尽管大多数人选择不练习这种特殊的艺术。无论如何,即使是编译器生成的字节码也可能不受信任。事实上,Ken Thompson 在他著名的 1984 年图灵奖演讲“反思信任信任”中更进一步地说:“你不能信任你没有完全自己创建的代码。”因此,JVM 必须能够决定一段字节码是否有效和可接受。
Java 字节码的每个单元(操作码)正好是一个字节长,并且定义明确。一个诚实的编译器接受有效的 Java 源代码,并生成一个字节码序列,该序列准确地反映了源代码的意图,同时保持了 Java 语言的固有属性,例如类型安全。另一方面,恶意生成的字节码序列可能根本不对应于任何有效的 Java 源代码,并且可能有意破坏语言属性以实现安全攻击。
判断一个呈现的操作码系列是否“有效”从根本上来说是一种输入验证问题。假设 JVM 将 1 到 9 范围内的任何整数都视为有效输入;那么输入验证就很简单了。实际上,包含任意操作码序列的输入空间大小是无限的,并且有效字节码序列稀疏地分布在其中。由于没有简单的验证测试公式可以通过它来运行目标代码,因此 Java 运行时系统在多个阶段、使用各种技术、在不同的地方进行字节码验证。字节码验证器静态检查传入的代码。一旦代码进入系统,类型安全机制就会部署在整个 JVM 中,以发现和阻止非法代码。所有这些操作都很复杂,并且在没有对整个系统进行形式化验证的情况下,无法确定所有可能的无效代码都可以被发现。
因此,对于任何处理从高级语言编译的可执行代码的运行时系统来说,一个真正困难的问题是确保从“外部”来源接收的代码是有效输入。对于大多数编程语言来说,这根本不可能。Java 的平台无关字节码使这项任务成为可能,但仍然极其难以正确完成。Brian Bershad 和他(当时)在华盛顿大学的学生 Emin Gun Sirer 提出了字节码破坏者4的概念。他们建立了一个自动化系统,生成随机的操作码序列来投掷到任何特定的 Java 运行时系统;他们观察系统是否崩溃,然后分析结果以找出 Java 实现中的缺陷。这种随机化和自动化的方法是一种成本出奇地低廉但有效的工具,JavaSoft 团队很快就采用了。
现在出现了防止不良意外行为的问题。例如,当 Java 应用程序或小程序触发对本地文件的访问请求时,该请求应该被授予吗?嗯,这取决于情况。如果请求是读取包含个人信用卡信息的本地文件,那么该请求可能会或可能不会被授予,这取决于是否有安全策略或用户偏好设置。如果请求是读取 12 号 Calibri 类型的字体文件,以便可以根据文字处理器规范显示文本文件,那么几乎总是应该授予该请求,因为隐式地,字体文件的结构决定了如果以这种方式使用,它们是无害的。请注意,应用程序永远不会直接打开文件。它们调用 Java 平台中的文件 API 来进行这些操作。这些 API 没有内置的安全概念,只是它们(或它们的设计者)知道文件操作很敏感,因此他们最好调用 SecurityManager 进行咨询。
这里问题又露出了丑陋的头——SecurityManager 被置于聚光灯下,却一丝不挂。例如,对于系统中编写的显示代码来说,访问字体文件是完全可以的,但通常情况下,应用程序直接访问系统字体文件是一个坏主意,因为这些文件可能会被任意更改,这可能会导致未来的显示问题。然而,SecurityManager 几乎无法区分这两种情况,更不用说无限数量的变体了。
在 JDK 1.0/1.1 中,非黑即白的沙箱安全模型或多或少按以下方式工作。SecurityManager 查找调用历史(方法调用的历史)。如果所有代码都是本地的(即,没有远程加载的因此不受信任的小程序),则访问请求被授予。如果调用链中存在小程序,则请求被拒绝,除非访问文件的直接调用者是系统代码;除非该代码不应该访问字体文件,但当小程序代码实际上处于回调情况时则不然;除非进行回调(到小程序)的系统代码实际上不应该访问字体文件,等等,等等。此外,关于线程、异常和其他混合或断开执行上下文的结构又如何呢?你明白了吧。
从根本上说,试图猜测程序的意图是不切实际的。最好要求程序员明确声明他们的意图,正如我们稍后将看到的那样。
更糟糕的是,SecurityManager 实现实际上并没有经历这种逻辑推理——它不能。相反,基于一些经验法则和 Java 系统的特定实例,SecurityManager 只是计算它自身与最近的小程序代码之间的距离——方法调用的数量——并启发式地决定是否应该授予请求。现在应该很明显,这种设置意味着,每当系统的某一部分发生更改时,启发式方法都可能变得错误,因此必须进行调整,无论启发式方法最初是否正确或完整,也无论使用何种方法来扩展系统以包含用户/主体等新概念来做出访问请求决策。这种脆弱的设置是许多安全漏洞的根源。
JDK 1.2 被完全推倒并重新架构,采用了众所周知但几乎从未实践过的最小特权原则。3所有代码都被平等对待。每段代码都被赋予一组特权(访问权限),无论是显式地(通过策略、管理或偏好设置)还是隐式地(系统代码具有完全特权,而小程序具有与其沙箱时代相同的特权级别)。在执行的任何时候,如果调用链上的每段代码都具有足够的特权来访问请求的资源,则访问请求被授予。换句话说,有效特权集是调用历史记录中所有代码的特权集的交集——最小特权原则。此外,与安全相关的上下文信息可以被封装并传递,这样就无法通过生成新线程或抛出异常来欺骗系统。当然,所有这些都假设实现安全机制的代码本身是安全和正确的。
一段代码——例如,可能不时需要访问字体文件的显示库——可以显式声明行使其自身的单方面特权,告诉安全系统忽略之前出现的代码。这种设计允许程序员,尤其是那些编写系统和库代码的程序员,在执行敏感操作时明确声明他们的意图。这类似于 Unix 中的 setuid 功能,但与为整个程序启用系统级高特权不同,在 Java 中,特权模式仅在特权方法调用期间持续存在。请注意,如果此特权代码稍后调用特权较低的代码,则由于最小特权原则,有效特权集仍将受到限制。
对显式声明的需求起初可能看起来很麻烦,但程序员可以放心,他们的代码不会无意中削弱安全性。此外,大多数程序不需要专门调用它们的特权,因此我们为最常见的编程情况提供了两全其美的方案——易于编码和安心。请注意,程序员可能不容易弄清楚他们到底需要声明哪些特权才能使他们的程序在所有可能的场景中正常工作。JDK 1.2 设计实际上不支持细粒度的特权声明。声明启用与代码关联的所有特权。
这里的主要教训是,系统化比临时凑合更容易且更健壮,尽管并非所有人都理解这一点。在 JDK 1.2 开发即将结束时,在对整个代码库进行安全审计期间(这在经过多次恳求,加上棍棒加大棒之后才得以实施),我们发现一位在 JDK 上工作的 Sun 工程师故意复制然后修改了系统代码,这样他自己的库代码就不必费心显式声明特权——此举可能使他的工作稍微容易了一些,但如果他的不当行为没有被发现,将会给 Java 平台的所有用户带来严重的安全漏洞。
该领域仍然存在许多难题。其中最重要的是,最小特权计算是否可以高效地完成,尤其是在安全参数非常复杂的情况下——例如,复杂的访问控制策略、许多不同类型的访问权限以及复杂的执行环境。另一个问题是,为不同的代码分配不同的特权会给系统的其他部分带来复杂性。例如,JIT(即时)编译器进行的优化现在必须符合额外的安全要求。
另一个长期存在的问题是安全策略管理和部署的实践方面。最后,一个更理论化的问题,但仍然值得思考的问题是,可以使用 JDK 1.2 中定义的相当传统的访问控制权限类型,在最小特权模型中强制执行的安全策略的范围是什么(或不能是什么)。康奈尔大学的 Fred Schneider 开发了一个名为内联引用监视器的有趣概念,并证明它可以表达和强制执行任何和所有可通过监视系统执行来强制执行的策略5。
尽管存在这些难题,但一个令人欣慰的想法可能是,经过在该领域 12 年多的发展,JDK 1.2 中架构的最小特权原则经受住了时间的考验,并且可能避免了无数的编码错误变成安全错误。
许多其他技术教训值得定期重复。例如,你应该非常谨慎地使用 NULL,因为你无法改变虚无的行为。在 JDK 1.0/1.1 中,在某些情况下,ClassLoader 或 SecurityManager 可能是 NULL,这使得难以改进更细粒度的设计。
再举一个例子,在运行时,Java 将静态代码转换为活动对象。这个过程实际上包含两个独立的步骤:(1)定位代码描述;(2)将其定义为活动对象。第一步本质上应该是开放和可扩展的,因为运行时系统和应用程序都应该能够指定获取代码的期望位置。另一方面,第二步必须受到严格控制,以便只有受信任的系统代码才能处理创建对象的工作。不幸的是,这两个步骤被重载到一个方法中,该方法在非黑即白模型中运行良好,但在 JDK 1.2 更改为更细致的世界时造成了很多困难。
另一个问题可能会让很多人感到惊讶:严格来说,Java 不能保证连续指令的顺序执行。一个简单的原因是可能会抛出异常,导致执行线程绕道(并可能永远不会返回)。一种补救措施是使用 Try/Finally 等子句来强制返回。在更极端的情况下——例如,当实际物理机器内存耗尽时——Java 运行时系统的行为是未定义的,并且肯定远非故障安全。由于许多关键的 JVM 功能(包括一些用于安全的功能)都是用 Java 编写的,因此这些情况变得更加复杂,因此系统的一部分出现问题很容易影响系统另一部分的正确性。对于所有这些设计挑战和替代方案,请参阅 Java 安全书籍2和最新的 Java 文档。
本文的其余部分讨论了对于以前的工作经验仅限于学术界的人来说完全出乎意料的挑战。科学家和工程师接受培训来解决技术问题,但现实世界的项目——尤其是像 Java 这样具有行业范围影响的项目——在社会和政治方面也同样重要。在 JDK 工作的约 30 个月中,我参加了大约 1000 次会议,并做了 300 多页的笔记。人们很容易忘记当时的战区氛围,尤其是周五的消防演习。安全研究人员经常在周五通知我们新发现的安全漏洞,并给我们时间到周一中午来找出补丁和响应,然后他们会通知纽约时报、华尔街日报和其他媒体。有时,在我们将补丁推出给 Java 许可证持有者(包括 IBM、Microsoft、Netscape 和许多其他公司)之后,泄露给记者的情况就会发生,我们只能猜测他们中的哪一个有动机在补丁到位之前公开安全漏洞。
然后还有各种其他同样耗时且耗费精力的干扰因素,例如美国对基本密码学的出口管制规定(现已放宽)、RSA 和公钥技术的专利(现已过期)以及代码混淆、用于电子商务和智能卡的 Java 以及 JavaOS 等问题。
为了确保我们走在正确的道路上,我们邀请了少数几位学术界和行业专家(包括 MIT 的 Jerome Saltzer 和 DEC 系统研究中心的 Michael Schroeder,最小特权原则论文的作者),并召集了一个正式的 Java 安全咨询委员会,该委员会在重新架构进展过程中提供了定期审查和宝贵的反馈。我们还从许多来源收到了很好的建议,主要是学术研究人员和行业合作伙伴——并非所有建议都是主动提出或友好的。一些头脑强硬的研究人员希望将他们的替代设计纳入 Java 平台,并向我们发出了各种威胁。
Netscape 是一个独特的故事。它是最流行的包含 Java 的浏览器,因此是一个有价值的合作伙伴;它对 Java 的发展方向也有自己的想法,这些雄心壮志使这种关系变得困难。在技术层面上,安全领域的主要争议在于 Netscape 的观点,即 Java 基本上只是一个浏览器组件,因此安全机制应该面向浏览器用户,而我们的愿景是 Java 是一个通用编程平台,应该满足各种用途,包括浏览器和服务器端应用程序。
在工程层面上,Netscape 每三个月进行一次创新和发布新版本,而 Sun/JavaSoft 则需要一年或两年才能发布一个带有新功能(例如 Netscape 请求的功能)的主要版本,这些功能将通过官方 JDK 平台提供。随着 JDK 中的 Java 代码与 Netscape 浏览器中的 Java 代码之间的差异变得难以管理,Netscape 和 JavaSoft 的总裁邀请 IBM 进行保密且具有约束力的仲裁。经过数月的密集事实调查、代码收集和消费者报告风格的评分,IBM 于 1997 年 10 月 15 日在 JavaSoft 街区外的一个 IBM Java 大楼召开了决议会议,并宣布 JavaSoft 的设计获胜。
多年后回顾,我至少可以看到 Java 安全工作的三个持久影响。最明显的是,新的安全架构为 Java 程序员提供了更好的支持,使他们的应用程序更安全,并降低了代码出现故障时的风险。其次,我们提高了所有其他人的标准,因为任何新的语言或平台都必须考虑类型安全、系统安全和最小特权原则,因为我们已经证明,即使在大型商业环境中,这些也是可以实现的。最后,Java 中的安全构造提高了成千上万开发人员的安全意识,然后他们可以将这些知识转移到其他编程语言和开发平台。
我要感谢卡内基梅隆大学的 Jeannette Wing、SRI International 的 Jeremy Epstein 和 Peter Neumann 以及剑桥大学的 Ross Anderson 和 Robert Watson 邀请我做那些关于 Java 安全的回顾性演讲。我感谢 的 Robert Watson 和 Jim Maurer 鼓励我将剑桥演讲写成C,并感谢那些深思熟虑的匿名审稿人。当然,我深深感谢所有关心、帮助和支持 Java 安全项目的人们。
1. Gong, L. 1997. Java security: present and near future. IEEE Micro (May): 14-19.
2. Gong, L., Ellison, G., Dageforde, M. 2003. Inside Java 2 Platform Security: Architecture, API Design and Implementation, second edition. Addison-Wesley.
3. Saltzer, J. H., Schroeder, M. D. 1974. The protection of information in computer systems. Communications of the 17 (7).
4. Sirer, E., Bershad, B. 1999. Testing Java Virtual Machines. Proceedings of the International Conference on Software Testing And Review (November).
5. Schneider, F.B. 2000. Enforceable security policies. Transactions on
Information and System Security (February): 30-50.
喜欢或讨厌?请告诉我们
李恭在学术研究、工业界和创业领域拥有丰富的经验。他曾担任 Sun Microsystems JavaSoft 部门的杰出工程师和首席 Java 安全架构师。他在 Java 安全方面的两项专利是 2010 年 Oracle 和 Google 之间关于 Android 的诉讼中重点关注的七项专利之一。他毕业于北京清华大学,并获得剑桥大学博士学位。他是 Mozilla Online Ltd.(总部位于北京的 Mozilla 子公司)的董事长兼首席执行官。
© 2011 1542-7730/11/0900 $10.00
最初发表于 Queue vol. 9, no. 9—
在 数字图书馆 中评论本文
Matt Godbolt - C++ 编译器中的优化
在向编译器提供更多信息方面需要权衡:这会使编译速度变慢。链接时优化等技术可以为您带来两全其美的效果。编译器中的优化不断改进,即将到来的间接调用和虚函数分派的改进可能很快就会带来更快的多态性。
Ulan Degenbaev, Michael Lippautz, Hannes Payer - 作为合资企业的垃圾回收
跨组件跟踪是一种解决跨组件边界的引用循环问题的方法。只要组件可以形成具有跨 API 边界的非平凡所有权的任意对象图,就会出现此问题。CCT 的增量版本已在 V8 和 Blink 中实现,从而能够以安全的方式有效且高效地回收内存。
David Chisnall - C 不是一种低级语言
在最近的 Meltdown 和 Spectre 漏洞之后,值得花一些时间来研究根本原因。这两个漏洞都涉及处理器推测性地执行超出某种访问检查的指令,并允许攻击者通过侧信道观察结果。导致这些漏洞以及其他几个漏洞的功能的添加是为了让 C 程序员继续相信他们在用低级语言编程,但这在几十年内一直不是这种情况。
Tobias Lauinger, Abdelberi Chaabane, Christo Wilson - 你不应该依赖我
大多数网站都使用 JavaScript 库,其中许多库已知存在漏洞。了解问题的范围,以及包含库的许多意外方式,只是改进情况的第一步。这里的目标是,本文中包含的信息将有助于为社区提供更好的工具、开发实践和教育工作。