且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

《Java安全编码标准》一3.1 DCL00-J防止类的循环初始化

更新时间:2022-09-27 12:51:29

3.1 DCL00-J防止类的循环初始化

在Java语言规范(Java Language Specification, JLS)第 12.4节“对类和接口的初始化”中提到[JLS 2005]:
对类进行的初始化包括执行该类的static静态初始化方法和初始化该类中的静态数据成员(类变量)。
换句话说,一个静态数据成员的出现会触发类的初始化。然而,一个静态数据成员可能会依赖于其他类的初始化,这样有可能形成一个初始化循环。在JLS的8.3.2.1节“类变量的初始化”中也提到[JLS 2005]:
在运行态中,使用编译期的常量来初始化的final的static变量,是最先初始化的。
这个JLS的描述会让人误解。误解之处在于,对于某些对象实例而言,变量就算是static final的,它们的初始化也可能会安排在后期进行。声明一个字段为static final并不能够保证它在被读之前已经完全初始化。
一般来说,程序,特别是对安全敏感的程序,必须消除所有的类初始化循环。

3.1.1 不符合规则的代码示例(类内循环)

不符合规则的代码示例如下,它存在一个涉及多个类的类初始化循环。

public class Cycle {
??private final int balance;
??private static final Cycle c = new Cycle();?
??// Random deposit
??private static final int deposit = (int) (Math.random() * 100);

??public Cycle() {
????balance = deposit – 10; // Subtract processing fee
??}

??public static void main(String[] args) {
????System.out.println("The account balance is: " + c.balance);
??}
}

Cycle类声明了一个private static final类变量,这个变量会在创建Cycle对象的时候进行初始化。静态的初始化方法可以保证只调用一次,而这次调用是在第一次使用静态类变量或者在第一次调用构造函数之前发生。
程序员希望通过在存款账户中减去处理费用来计算账户余额。然而,对类变量c的初始化在deposit变量被初始化之前发生,因为在编码上,它在deposit域的初始化之前出现。因此,当对变量C进行静态初始化时,Cycle类的构造函数会读取deposit的数值,这个deposit数值会是0而并非一个随机值。结果是,账户余额计算下来的值是-10。
JLS允许实现忽略这种可能出现的循环的初始化[Bloch 2005a]。

3.1.2 符合规则的方案(类内循环)

这个方案改变了类Cycle的初始化次序,所以对数据成员的初始化是不会造成任何依赖循环的。特别是对c的初始化,因为在编码上处于对deposit初始化之后发生,因而它会在deposit被完全初始化之后进行初始化。

public class Cycle {
??private final int balance;
??// Random deposit
??private static final int deposit = (int) (Math.random() * 100);?
??// Inserted after initialization of required fields
??private static final Cycle c = new Cycle();??
??public Cycle() {
????balance = deposit - 10; // Subtract processing fee
??}

??public static void main(String[] args) {
????System.out.println("The account balance is: " + c.balance);
??}
}

当涉及许多字段的时候,并不容易发现这样的初始化循环。因此,确保控制流不产生这样的循环就非常重要。
尽管这个方案可以防止初始化循环,但它也依赖于声明次序,因而也是脆弱的。程序的维护者可能不知道这种声明的次序必须被保持来保证程序的正确性。因而,这种依赖必须在代码的文档中清楚地说明。

3.1.3 不符合规则的代码示例(类间循环)

这个不符合规则的代码示例声明了两个类,这两个类都有静态变量,这些变量的值是互相依赖的。当把这两个类放在一起的时候,这个循环是很显然的;而当把它们分开的时候,却很容易忽略这点。

class A {
??public static final int a = B.b + 1;
??// ...
}

class B {
??public static final int b = A.a + 1;
??// ...
}

因为对这些类的初始化次序是可变的,所以导致的结果是,会计算出不同的A.a?和B.b的值。当先初始化A类时,A.a的值是2,而B.b的值是1。当先初始化B类时,这两个值就要反过来了。

3.1.4 符合规则的方案(类间循环)

与规则一致的方案打破了这个类间循环,这是通过消除其中一个依赖来完成的。

class A {
??public static final int a = 2;
??// ...
}
// class B unchanged: b = A.a + 1

当打破这个循环时,初始化的值是不变的,总是A.a=2且B.b=3,并且它与初始化次序无关。

3.1.5 风险评估

初始化循环会导致不可预测的结果。
《Java安全编码标准》一3.1 DCL00-J防止类的循环初始化

3.1.6 相关规范

《Java安全编码标准》一3.1 DCL00-J防止类的循环初始化

3.1.7 参考书目

《Java安全编码标准》一3.1 DCL00-J防止类的循环初始化
《Java安全编码标准》一3.1 DCL00-J防止类的循环初始化