技巧:防范代码的终结器漏洞
原作者:
来源Tip: Secure your code against the finalizer vulnerability
译者Elaine.Ye
在使用终结器(finalizer)来创建对象时,其可能会给Java代码带来一个脆弱而易受攻击的点。该漏洞是这一为大家所熟知的使用终结器来复活对象技术的一种变体。当一个有着finalize()方法的对象变得无法访问时,其就被放置在某个队列中,在晚些时候再加以处理。本篇文章解释了这一漏洞是如何起作用的,并给你展示了如何保护你的代码免受其危害,所有的这些代码例子都提供了下载。
终结器的想法是允许Java方法释放任何需要返还给操作系统的本地资源,遗憾的是,任何的Java代码都可以在终结器中运行,其允许诸如清单1中的这样代码的执行:
清单1. 一个可复活的类
public class Zombie {
static Zombie zombie;
public void finalize() {
zombie = this;
}
}
当Zombie的终结器被调用时,其用到了被终结的对象——通过this来引用——并把它保存在zombie这一静态变量中。现在该对象又是可访问的,其不能被垃圾收集。
这一代码的一个更加隐秘的版本甚至允许一个只进行了部分构造的对象被复活,即使对象在初始化过程中不能通过正确性检查,其仍能够被终结器创建出来,如清单2中的代码:
清单2. 创建一个非法的类
public class Zombie2 {
static Zombie2 zombie;
int value;
public Zombie2(int value) {
if(value < 0) {
throw new IllegalArgumentException("Negative Zombie2 value");
}
this.value = value;
}
public void finalize() {
zombie = this;
}
}
在清单2中,对value参数进行检查的效果被finalize()方法的存在给抵消掉了。
攻击是如何生效的
当然,不可能会有人写出清单2那样的代码,但如果类被继承了的话,漏洞就有可能出现,如清单3中的类:
清单3. 一个易受攻击的类
class Vulnerable {
Integer value = 0;
Vulnerable(int value) {
if(value <= 0) {
throw new IllegalArgumentException("Vulnerable value must be positive");
}
this.value = value;
}
@Override
public String toString() {
return(value.toString());
}
}
清单3中的Vulnerable类的目的是防止value的值被设置成一个非正数,但这一目的被AttackVulnerable()方法给颠覆了,如清单4所示:
清单4. 一个破坏了Vulnerable类的类
class AttackVulnerable extends Vulnerable {
static Vulnerable vulnerable;
public AttackVulnerable(int value) {
super(value);
}
public void finalize() {
vulnerable = this;
}
public static void main(String[] args) {
try {
new AttackVulnerable(-1);
} catch(Exception e) {
System.out.println(e);
}
System.gc();
System.runFinalization();
if(vulnerable != null) {
System.out.println("Vulnerable object " + vulnerable + " created!");
}
}
}
AttackVulnerable类的main()方法试图创建一个新的AttackVulnerable对象实例,因为value的值超出了范围,因此异常被抛出且在catch块中捕捉到。System.gc()和System.runFinalization()的调用促使VM运行一个垃圾回收周期并运行一些终结器,这些调用并非是攻击成功的必需因素,不过它们可用来说明攻击的最终结果,结果是Vulnerable对象被创建了出来,有着一个无效的值。
测试用例的运行给出了如下的结果:
java.lang.IllegalArgumentException: Vulnerable value must be positive
Vulnerable object 0 created!
为什么Vulnerable的值是0而不是-1呢?可以注意到,在清单3的Vulnerable构造函数中,对value的赋值是在参数通过检查之后才会发生的,因此value拥有的是初始值,在这一例子中是0。
这种攻击甚至可以用来绕过显式的安全检查,例如,清单5中的Insecure类的设计想法是,如果其运行在一个SecurityManager的管理之下,且调用者没有权限写入当前目录的话就抛出一个SecurityException。
清单5. Insecure类
import java.io.FilePermission;
public class Insecure {
Integer value = 0;
public Insecure(int value) {
SecurityManager sm = System.getSecurityManager();
if(sm != null) {
FilePermission fp = new FilePermission("index", "write");
sm.checkPermission(fp);
}
this.value = value;
}
@Override
public String toString() {
return(value.toString());
}
}
清单5中的Insecure类可受到攻击的方式和前面的一样,清单6在AttackInsecure类中给出了代码:
清单6. 攻击Insecure类
public class AttackInsecure extends Insecure {
static Insecure insecure;
public AttackInsecure(int value) {
super(value);
}
public void finalize() {
insecure = this;
}
public static void main(String[] args) {
try {
new AttackInsecure(-1);
} catch(Exception e) {
System.out.println(e);
}
System.gc();
System.runFinalization();
if(insecure != null) {
System.out.println("Insecure object " + insecure + " created!");
}
}
}
运行在SecurityManager之下的清单6代码给出了如下的输出:
java -Djava.security.manager AttackInsecure
java.security.AccessControlException: Access denied (java.io.FilePermission index write)
Insecure object 0 created!
如何避免攻击
直到Java语言规范( Java Language Specification,JLS)的第三版在Java SE 6中实现后,才有了这些避免攻击的方法——使用initialized标志,禁止子类化或是创建一个以final修饰的终结器——不算是令人满意的解决方案。
使用initialized标志
一种避免攻击的方法是使用initialized标志,一旦对象被正确创建该标志就设为true。类中的每个方法先查看initialized标志是否已经设置,如果没有的话就抛出异常。这种编码方式写起来很烦人,意外的忽略很容易就发生,并且不能阻止攻击者子类化方法。
防止子类化
你可以把所创建的类声明为final的,这意味着没有人能够创建该类的子类,从而阻止了攻击的得以进行。然而,这一技巧消除了类的灵活性,使得不能够通过扩展类来特殊化它或是添加额外的功能。
创建一个final终结器
你可以为正在创建的类创建一个终结器并把它声明成final的,这意味着该类的任何子类都不能再声明终结器。这一方法的缺点是终结器的存在意味着对象保持存活的时间长于其原本应该存活的时间。
一种更新的、更好的做法
为了在不引入额外的代码或是限制的条件下更容易地阻止这类攻击,Java的设计者修改了JLS(参见参考资料),规定如果java.lang.Object在构建之前有异常从构造函数中抛出的话,类的finalize()方法将不会被执行。
但在java.lang.Object被构造之前如何可能会有异常被抛出呢?别忘了,任何构造函数的第一行必须是对this()或是super()的调用,如果构造函数没有包括这样的显式调用的话,一个对super()的调用就会被隐含地添加进来。因此在对象被创建之前,同一个类或是它的父类的另一个对象必须是已构造好了的。因此在任何来自构造中的方法的代码执行之前,这一规定最终会把执行引向 java.lang.Object自身的构造函数,然后是所有子类的构造函数。
要理解异常如何能够在java.lang.Object被构造之前抛出的话,你需要了解对象构造的确切顺序,JLS明确地解释了这一顺序。
在对象被创建时,JVM:
1. 为对象分配空间。
2. 把对象中的所有实例变量的值设置成它们的默认值,这包括了对象的父类中的实例变量。
3. 对对象参数变量进行赋值。
4. 处理任何显式的或是隐式的构造函数调用(构造函数中的this()或是super()调用)。
5. 初始化类中的变量。
6. 执行构造函数中的其余部分代码。
关键的一点是,构造函数的参数处理是在构造函数内部的任何代码处理之前进行的,这意味着如果你在处理参数时进行验证的话,你可以——以抛出异常的方式——阻止类被终结。
这带来了清单3中的 Vulnerable 类的一个新版本,如清单7所示:
清单7. Invulnerable类
class Invulnerable {
int value = 0;
Invulnerable(int value) {
this(checkValues(value));
this.value = value;
}
private Invulnerable(Void checkValues) {}
static Void checkValues(int value) {
if(value <= 0) {
throw new IllegalArgumentException("Invulnerable value must be positive");
}
return null;
}
@Override
public String toString() {
return(Integer.toString(value));
}
}
在清单7中,Invulnerable的公有构造函数调用了一个私有的构造函数,该函数调用checkValues方法来创建它的参数。该方法在构造函数做构造超类的调用之前被调用,这一超类的调用就是Object的构造函数。因此如果异常从checkValue中抛出的话,Invulnerable对象就不会进行终结操作。
清单8中的代码试图攻击Invulnerable:
清单8. 试图破坏Invulnerable类的尝试
class AttackInvulnerable extends Invulnerable {
static Invulnerable vulnerable;
public AttackInvulnerable(int value) {
super(value);
}
public void finalize() {
vulnerable = this;
}
public static void main(String[] args) {
try {
new AttackInvulnerable(-1);
} catch(Exception e) {
System.out.println(e);
}
System.gc();
System.runFinalization();
if(vulnerable != null) {
System.out.println("Invulnerable object " + vulnerable + "
created!");
} else {
System.out.println("Attack failed");
}
}
}
//增加的部分内容
// } else {
// System.out.println("Attack failed");
如果是使用根据较旧的JLS版本编写的Java 5的话,Invulnerable对象就会被创建出来:
java.lang.IllegalArgumentException: Invulnerable value must be positive
Invulnerable object 0 created!
Java SE 6(从Oracle的JVM和IBM JVM的SR9的通用版发布开始)遵循了最新的规范,因此对象没有被创建:
java.lang.IllegalArgumentException: Invulnerable value must be positive
Attack failed
结论
终结器是Java语言中一个令人遗憾的功能,尽管垃圾收集器可以自动地回收任何不再被Java对象使用的内存,但并没有存在这样的机制来回收诸如本地内存、文件描述符或是套接口一类的本地化资源。Java提供的与这些本地化资源做接口的标准库通常会有一个close()方法来允许适当的清理——但它们还需要使用终结器来确保在对象没有被正确关闭时不会有资源泄漏的情况发生。
就其他对象来说,一般情况下最好避免使用终结器,因为并不存在这样的保证,即终结器会在某个时候被执行,甚至到底会不会被执行也不能保证。终结器的存在意味着除非终结器已被运行,否则一个不能被访问的对象不能被垃圾收集,,且这一对象甚至有可能会让更多的对象保持在存活的状态中。这就导致了活动对象数目的增加,从而导致了Java进程的堆使用的增加。
终结器复活了一个预定要被垃圾收集的对象,这一能力显然是终结操作机制运作方式的一个意想不到的后果。现在JVM的较新实现允许你保护代码免遭这一后果所带来的安全隐患的威胁。
下载
描述 名称 大小 下载方法
这一编程技巧的代码例子 j-fv.zip.zip 4KB HTTP
参考资料
学习资料
1. Java Language Specification:有关Java语言的技术参考。
2. Secure Coding Guidelines for the Java Programming Language:阅读这些准则来获得更多关于良好的编程习惯的建议。
3. Effective Java, 2d ed. (Joshua Bloch,Prentice Hall,2008):本书包括了与终结器以及其他的一些重要方面相关的问题的一个讨论。
4. Language designer's notebook:了解Brian Goetz的developerWorks系列文章,这一系列谈论的是影响到Java语言未来的语言设计问题。
5. Java theory and practice:浏览Brian Goetz的这一长期开设的developerWorks系列,该系列是关于Java编程概念、技巧和最佳做法的。
6. developerWorks Java technology zone:可找到数百篇关于Java编程的各个方面的文章。
获取产品和技术
1. 以最适合你的方式来评估IBM的产品:下载产品试用版、在线试用产品、使用云环境中的产品,或是在SOA Sandbox中花费几个小时来学习如何有效地实现面向服务的架构(Service Oriented Architecture)。
讨论
1. Java security:参加developerWorks上帝Java安全论坛。
2. 加入developerWorks社区:在浏览开发者驱动的博客、论坛、讨论组和wiki时与其他developerWorks用户建立联系。
关于作者
Neil Masson多年来一直都在从事Java语言方面的开发和支持工作,目前他的工作重点是改进Java发行版本的质量和安全。
相关文章:
RJC301:Web开发——Tomcat、GlassFish、OSGi、Tapestry等服务器和框架中的Classloader