如果你正在使用 Java 开发安全程序,坦率地说,你的首要步骤(在学习 Java 之后)是阅读两本主要的 Java 安全著作,即 Gong [1999] 和 McGraw [1999](后者尤其关注第 7.1 节)。 你还应该查看 Sun 发布的安全代码指南,网址为 http://java.sun.com/security/seccodeguide.html, 还有一篇 Sahu 等人 [2002] 发表的优秀文章。 一组描述 Java 安全模型的幻灯片可在以下网址免费获取:http://www.dwheeler.com/javasec。 你也可以参考 McGraw [1998]。
显然,很大程度上取决于你正在开发的应用程序类型。 客户端使用的 Java 代码与服务器端代码具有完全不同的环境(和信任模型)。 当然,一般原则仍然适用; 例如,你必须检查和过滤来自不可信来源的任何输入。 但是,在 Java 中,有一些“隐藏的”输入或潜在输入需要你警惕,如下所述。 Johnathan Nightingale [2000] 发表了一个有趣的声明,总结了 Java 编程中的许多问题:
... Java 编程中最重要的事项是注意你的继承关系。 如果你从父类、接口或父接口继承方法,你可能会为你的代码打开后门。
以下是一些关键指南,基于 Gong [1999]、McGraw [1999]、Sun 的指南以及我自己的经验
不要使用公共字段或变量; 将它们声明为私有,并提供访问器来限制它们的可访问性。
除非有充分的理由,否则将方法设为私有(如果你不这样做,请记录原因)。 这些非私有方法必须保护自己,因为它们可能会接收到被污染的数据(除非你以某种方式安排了保护它们)。
JVM 可能实际上不会在应用程序中(与 applet 相反)强制执行可访问性修饰符(例如,“private”)。 感谢 John Steven (Cigital Inc.),他在 2000 年 11 月 7 日的“安全编程”邮件列表中指出了这一点。 问题在于,这完全取决于请求访问的类是由哪个类加载器加载的。 如果该类是由受信任的类加载器(包括 null/原始类加载器)加载的,则访问检查返回“TRUE”(允许访问)。 例如,这可行(至少对于 Sun 的 1.2.2 VM;可能不适用于其他实现)
编写一个带有公共字段的受害者类 (V),编译它。
编写一个访问该字段的“攻击”类 (A),编译它
将 V 的公共字段更改为私有,重新编译
运行 A - 它将访问 V 的(现在是私有的)字段。
但是,对于 applet,情况有所不同。 如果你将 A 转换为 applet 并将其作为 applet 运行(例如,使用 appletviewer 或浏览器),则其类加载器不再是受信任的(或 null)类加载器。 因此,代码将抛出 java.lang.IllegalAccessError,并显示你正在尝试从类 A 访问字段 V.secret 的消息。
避免使用静态字段变量。 这样的变量附加到类(而不是类实例),并且任何其他类都可以找到类。 因此,任何其他类都可以找到静态字段变量,这使得它们更难以保护。
永远不要将可变对象返回给潜在的恶意代码(因为代码可能会决定更改它)。 请注意,数组是可变的(即使数组内容不是),因此不要返回对包含敏感数据的内部数组的引用。
永远不要直接存储用户给定的可变对象(包括对象数组)。 否则,用户可以将对象交给安全代码,让安全代码“检查”对象,并在安全代码尝试使用数据时更改数据。 在内部保存数组之前克隆数组,并在此处小心(例如,注意用户编写的克隆例程)。
不要依赖初始化。 有几种方法可以分配未初始化的对象。
将所有内容都设为 final,除非有充分的理由不这样做。 如果类或方法是非 final 的,攻击者可能会尝试以危险且不可预见的方式扩展它。 请注意,这会导致可扩展性的丧失,以换取安全性。
不要依赖包作用域来保证安全性。 少数类(如 java.lang)默认是封闭的,并且某些 Java 虚拟机 (JVM) 允许你关闭其他包。 否则,Java 类不是封闭的。 因此,攻击者可能会在你的包中引入一个新类,并使用这个新类来访问你认为你正在保护的东西。
不要使用内部类。 当内部类被翻译成字节码时,内部类被翻译成包中任何类都可以访问的类。 更糟糕的是,封闭类的私有字段会悄悄地变为非私有,以便允许内部类访问!
最小化权限。 在可能的情况下,根本不需要任何特殊权限。 McGraw 更进一步,建议不要对任何代码进行签名; 我说继续对代码进行签名(以便用户可以决定“仅运行来自此发件人列表的签名代码”),但尝试编写程序,使其仅需要沙箱权限集。 如果你必须拥有更多权限,请特别严格地审核该代码。
如果你必须对代码进行签名,请将其全部放在一个存档文件中。 这里最好引用 McGraw [1999]:
此规则的目标是防止攻击者执行混合搭配攻击,其中攻击者构建一个新的 applet 或库,该 applet 或库将你的一些签名类与恶意类链接在一起,或者将你不希望一起使用的签名类链接在一起。 通过将一组类一起签名,你使这种攻击更加困难。 现有的代码签名系统在防止混合搭配攻击方面做得不够充分,因此此规则无法完全防止此类攻击。 但是使用单个存档文件不会有坏处。
使你的类不可克隆。 Java 的对象克隆机制允许攻击者在不运行其任何构造函数的情况下实例化一个类。 要使你的类不可克隆,只需在你的每个类中定义以下方法
public final Object clone() throws java.lang.CloneNotSupportedException { throw new java.lang.CloneNotSupportedException(); } |
如果你真的需要使你的类可克隆,那么你可以采取一些保护措施来防止攻击者重新定义你的 clone 方法。 如果你正在定义自己的 clone 方法,只需将其设为 final。 如果你没有,你至少可以通过添加以下内容来防止 clone 方法被恶意覆盖
public final void clone() throws java.lang.CloneNotSupportedException { super.clone(); } |
使你的类不可序列化。 序列化允许攻击者查看你的对象的内部状态,甚至是私有部分。 为了防止这种情况,请将此方法添加到你的类中
private final void writeObject(ObjectOutputStream out) throws java.io.IOException { throw new java.io.IOException("Object cannot be serialized"); } |
即使在序列化可以的情况下,也请务必对包含系统资源的直接句柄以及包含与地址空间相关的信息的字段使用 transient 关键字。 否则,反序列化类可能会允许不当访问。 你可能还想将敏感信息标识为 transient。
如果你为类定义了自己的序列化方法,则它不应将内部数组传递给任何采用数组的 DataInput/DataOuput 方法。 理由是:所有 DataInput/DataOutput 方法都可以被覆盖。 如果一个 Serializable 类将一个私有数组直接传递给 DataOutput(write(byte [] b)) 方法,那么攻击者可以子类化 ObjectOutputStream 并覆盖 write(byte [] b) 方法,以使其能够访问和修改私有数组。 请注意,默认序列化不会将私有字节数组字段暴露给 DataInput/DataOutput 字节数组方法。
使你的类不可反序列化。 即使你的类不可序列化,它仍然可能是可反序列化的。 攻击者可以创建一个字节序列,该序列恰好反序列化为你的类的一个实例,并带有攻击者选择的值。 换句话说,反序列化是一种公共构造函数,允许攻击者选择对象的状态 - 显然是一个危险的操作! 为了防止这种情况,请将此方法添加到你的类中
private final void readObject(ObjectInputStream in) throws java.io.IOException { throw new java.io.IOException("Class cannot be deserialized"); } |
不要按名称比较类。 毕竟,攻击者可以定义具有相同名称的类,如果你不小心,你可能会因授予这些类不必要的权限而造成混淆。 因此,以下是确定对象是否具有给定类的错误方法示例
if (obj.getClass().getName().equals("Foo")) { |
如果你需要确定两个对象是否具有完全相同的类,请改用两侧的 getClass() 并使用 == 运算符进行比较。 因此,你应该使用这种形式
if (a.getClass() == b.getClass()) { |
if (obj.getClass() == this.getClassLoader().loadClass("Foo")) { |
此指南来自 McGraw 和 Felten,这是一个很好的指南。 我要补充一点,在可能的情况下,最好避免比较类值。 最好尝试设计类方法和接口,这样你根本不需要这样做。 但是,这并不总是可行的,因此了解这些技巧很重要。
不要将机密信息(加密密钥、密码或算法)存储在代码或数据中。 恶意的 JVM 可以快速查看此数据。 代码混淆并不能真正对认真的攻击者隐藏代码。