一、开发安全风险评估
序 | 开发安全规范 | 严重性 | 整改代价 | 优先级 | 级别 |
---|---|---|---|---|---|
1 | 在返回引用之前,防御性复制私有的可变的类成员 | 高 | 中 | 12 | 1 |
2 | 小心处理构造函数抛出异常的情况 | 高 | 中 | 12 | 1 |
3 | 声明数据成员为私有并提供可访问的封装器方法 | 中 | 中 | 12 | 1 |
4 | 比较类而不是进行类名称 | 高 | 低 | 9 | 2 |
5 | 不允许敏感类复制其自身 | 中 | 中 | 8 | 2 |
6 | 不要在嵌套类中暴露外部类的私有字段 | 中 | 中 | 8 | 2 |
7 | 不要使用公有静态的非final变量 | 中 | 中 | 8 | 2 |
- 级别:L1高危害、可利用性强、整改代价低;
- L2中危害、可能被利用、整改代价中;
- 优先级:数值越高,越优先修复;
- 来源:Java安全编码标准/朗(Long, F.)等著,第六章
二、开发安全规范说明
1、在返回引用之前,防御性复制私有的可变的类成员
返回类的内部可变成员的引用,会破坏一个应用的安全性,这个破坏既体现在对封装性的破坏,也体现在为破坏类的内部状态提供了可能性。因此程序禁止返回内部可变类成员的引用。
返回一个指向可变内部状态的防御性复制的引用,能保证调用者不会修改初始的内部状态,尽管这个副本仍然是可变的。
- 【情况1】返回私有对象引用或包含不可变对象的可变成员,使用
clone()
返回对象的克隆副本。
Public class Test_1_1 {
Private Date date; // 对象引用
Private HashMap hm = new HashMap(); // 包含不可变对象的可变成员
Public Date getDate(){
return (Date)date.clone();
}
Public String getString(int i){
Return hm.get(i); // 直接return hm是不安全的。
}
Public HashMap getMap() {
return (HashMap) hm.clone();
}
}
注意:若类会被非受信代码扩展时,在执行一个构造方法的防御性复制时,必须避免使用clone()
方法,改用原始的“创建对象,逐一赋值”的方法,防止执行被恶意覆写了的clone()
方法。
- 【情况2】返回私有可变对象数组,因为数组中包含的对象引用是可变的,对数组进行浅复制是不够的,需采用深度复制。
Public class Test_1_2 {
Private Date[] dates;
Public Date[] getDates(){
Date[] newDates = new Date[dates.length];
For(int i = 0; i < dates.length; i++){
newDates[i] = dates[i].clone();
}
Return newDates;
}
}
2、小心处理构造函数抛出异常的情况
在一个构造方法开始创建对象,但并未结束时,对象只会被部分地初始化。
其他类可能会从并发运行的线程中访问一个部分初始化的对象,在没有原子性失效的时候,部分初始化会导致对象处于不一致的状态。
处理部分初始化对象的问题有3种通用的解决方法:
在对象的构造方法中抛出异常。遗憾的是,攻击者会恶意地获得这样一个对象的实例。例如,一个使用终止器构造方法的攻击允许攻击者调用类中的任意方法,即使这个类方法是由安全管理器保护的。
声明对象的被初始化变量为final可以防止对象被部分初始化。当一个线程中执行的构造方法将一个final数据成员初始化成一个已知的安全值时,另外的线程不能看到这个对象任何初始化之前的值。
初始化标志。这个方法允许未初始化或者部分初始化的对象在已知的失效状态下存在,这样的对象就是我们通常认为的僵尸对象(zombie object)。这个方案容易出错,因为任何对这样一个类的访问必须要先检查对象是否被正确地初始化。
方案 | 未初始化值 | 部分初始化值 |
---|---|---|
构造函数中的异常 | 阻止 | 不阻止 |
Final数据成员(推荐) | 阻止 | 阻止 |
初始化标志 | 检测 | 检测 |
3、声明数据成员为私有并提供可访问的封装器方法
控制声明为public
或者protected
的数据成员的访问方式是很困难的,攻击者会用意想不到的方式控制这些成员。
因此,数据成员必须声明为private
,并使用public
或者protected
的封装器方法提供数据访问,监视并控制对数据成员的修改,保持类的不变性。
当数据成员是私有可变对象的引用时,要特别注意不要直接返回引用,详见规则1。
【例外】当类仅作为数据结构,而没有任何行为时,可以声明数据成员为public。
4、比较类而不是进行类名称
在JVM中, “如果它们被同一个类装载器装载,并且有相同的全名,那么这两个类被认为是相同的类(并且因此有相同的类型)”。
但是,有同样名称却来自不同包名的两个类是不同的类,因此不能基于类名称进行比较。
- 不符合规则的案例1:
// 基于类名称进行比较是不准确的
a.getClass().getName().equals(b.getClass().getName())
- 不符合规则的案例2:
// 比较类的全限定名也是不够的,
// 因为不同的类装载器会装载具有相同全限定名的不同的类到一个JVM中。
a.getClass().getName().equals(“com.test.a”)
- 符合规则的案例1:
a.getClass() == this.getClass().getClassLoader().loadClass(“com.test.a”);
- 符合规则的案例2:
a.getClass() == b.getClass()
5、不允许敏感类复制其自身
最好不要复制包含私有、保密或者其他敏感数据的类。
如果一个类是不准备被复制的,但它又没有定义复制机制,只通过构造函数进行安全检查,是不足以防止复制的。
Java的对象克隆机制clone()允许攻击者通过复制已有对象的内存镜像来创建一个类的新实例,而不是通过执行这个类的构造方法来创建新实例。
利用该机制,通过创建子类(构造方法中绕过父类的安全检测)和克隆子类实现敏感类的复制。
符合规则的方案1:
最容易防止利用恶意子类克隆的方法是声明类为final。符合规则的方案2:
定义一个总是抛出CloneNotSupportedException
错误的final clone
方法,防止子类成为可克隆的。
6、不要在嵌套类中暴露外部类的私有字段
嵌套类指那些声明在另一个类或者接口代码块中的类,嵌套类可以访问外被类的私有变量。
当嵌套类被声明为public或者当它包含public的方法或者构造方法时,可以被任何在同一个包里的其他类访问。
因此,嵌套类禁止将外部类的私有成员暴露给外部的类或者包。
- 不符合规则的案例:
class Test_7_1 {
Private int secret;
Public class InnerClass { // 嵌套类
Public int get(){ return secret; }
}
}
Class Test_7_2 {
Public static void main(String[] args) {
Test_7_1 t = new Test_7_1();
Test_7_1.InnerClass c = t. new InnerClass();
c.get(); // 这里获得Test_7_1的私有变量secret
}
}
- 符合规则的案例:
将InnerClass
声明为private
来隐藏嵌套类,或将可能暴露私有成员的方法声明为private
。
7、不要使用公有静态的非final变量
客户代码可以轻易地访问到公有静态数据成员。
安全管理器不会对读取或写入这些变量进行检查。此外,在将新值存储到这些字段之前,是不能通过编程方式进行验证的。
在多线程的场合中,非final的公有静态字段会被不一致的方式修改。因此,类必须不能包含非final的公有静态字段。
符合规则的案例:
public static final FuncLoader m_functions;