Java
如果要用Java来开发安全程序,坦白地说,第一步(在学习了Java之后)就是要阅读两本有关Java安全的教材,即Gong [1999]和McGraw [1999](后一本特别要看第7.1节)。还应该看一下Sun发布的安全代码指南http://java.sun.com/security/seccodeguide.html。有一组描述Java安全模型的幻灯片可以从 http://www.dwheeler.com/javasec 免费获取。
下面是基于Gong [1999]、McGraw [1999]和Sun的指南的若干关键要点:
不要使用公共域或变量;把它们声明为私有的并提供访问函数以限制对它们的访问。
除非有很好的理由,把方法都设为私有的(如果确实没这样做,说清楚其理由)。这些非私有的方法必须保护自己,因为它们可能会接收到受污染的数据(除非已经用其它方式对它们进行了保护)。
避免使用静态域变量。这样的变量附着在类(而非类的实例)上,而类可以被其它类所定位。其结果就是静态域变量可以被其它类找到,因此很难保证它们的安全。
永远不要把可变对象返回给潜在有恶意的代码(因为代码可能会改变它)。注意,数组是可变的(即使数组的内容不可变),所以不要返回一个含有敏感数据的内部数组的引用。
永远不要直接保存用户给定的可变对象(包括对象的数组)。否则,用户可以把对象交给安全代码,让安全代码“检查”对象,并在安全代码试图使用数据时改变数据。应该在内部存储数组前复制它们,而且要小心(例如,警惕用户编写的复制例程)。
不要依赖于初始化。有好几种方法给未初始化的对象分配内存。
除非有很好的理由,应该使每件事都是确定的。如果某个类或方法不是确定的,攻击者就可以用某种危险而无法预知的方法来扩展它。注意,作为安全性的交换,这会带来可扩展性的丧失。
不要在安全性上依赖包的范围。若干类,如java.lang,缺省是关闭的,而且某些Java虚拟机(JVM)会让你关闭其它包。否则,Java类是没有关闭的。因此,攻击者可以向包中引入一个新类,并用此新类来访问你以为保护了的信息。
不要使用内部类。在内部类转换为字节代码时,内部类会转换为可以访问包中任意类的类。更糟的是,被封装类的私有域静悄悄地变成非私有的,允许内部类访问!
最小化特权。如果可能,完全不要请求任何特殊的许可。McGraw更进一步地推荐不要标记任何代码;我认为可以标记代码(这样用户可以决定“只有列表上的发送者可以运行标记过的代码”),但在编写程序时要使程序不需要沙箱设置之外的权限。如果一定要有更大的权限,审读代码就会特别困难。
如果一定要标记代码,应该把它们都放在一个档案文件里。这里最好引用McGraw [1999]的原文:
此规则的目的是防止攻击者使用混合匹配攻击,构建新applet或库把某些标记类与有恶意的类连接在一起,或者把根本意识不到会被一起使用的标记类连接在一起。通过把一组类标记在一起,就可以使这种攻击更困难。现有的代码标记系统在防止混合匹配攻击上做得还不够,所以这一规则还不能完全防止此类攻击。但使用单个答案没什么坏处。
应该使类不可被复制。Java的类复制机制允许攻击者不运行构建函数就实例化某个类。要使类不可被复制,只要在每个类里定义如下方法: public final void clone() throws java.lang.CloneNotSupportedException {
throw new java.lang.CloneNotSupportedException();
}
如果确实需要使类可被复制,那么可以采用几个保护措施来防止攻击者重新定义复制方法。如果是定义自己的复制方法,只需要使它是确定的。如果不是定义自己的复制方法,至少可以通过增加如下内容来防止复制方法被恶意地重载: 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");
}
甚至在序列化没问题的情况下,也应该对包含直接处理系统资源的域和包含与地址空间有关信息的域使用临时关键字。否则,解除类的序列化就会允许不适当的访问。可能还需要把敏感信息标识为临时的。
如果对类定义了自己的序列化方法,就不应该把内部数组传递给需要数组的DataInput/DataOuput方法。其理由在于:所有的DataInput/DataOuput方法都可以被重载。如果某个可序列化的类向某个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()) {
如果确实需要判断某个对象是否含有某个给定类名,需要严格按照规范并确保使用当前名称空间(当前类的ClassLoader所在名称空间)。因此,应该使用如下形式: if (obj.getClass() == this.getClassLoader().loadClass("Foo")) {
本原则来自McGraw和Felten,而且确实是个好原则。要补充的是,尽可能地避免比较类值通常是个好注意。通常最好是尽力设计类的方法和接口,从而完全不必要做这些事。尽管如此,实际上无法完全做到,所以知道这些技巧还是很重要的。
不要把秘密(密钥、密码或算法)存储在代码或数据里。有恶意的JVM可以迅速看到这一数据。打乱代码并不能在认真的攻击者面前实际隐藏代码。