本节书摘来异步社区《Java编码指南:编写安全可靠程序的75条建议》一书中的第1章,第1.15节,作者:【美】Fred Long(弗雷德•朗), Dhruv Mohindra(德鲁•莫欣达), Robert C.Seacord(罗伯特 C.西科德), Dean F.Sutherland(迪恩 F.萨瑟兰), David Svoboda(大卫•斯沃博达),更多章节内容可以访问云栖社区“异步社区”公众号查看。
不可信代码可以滥用可信代码提供的API来覆盖一些方法,如Object.equals()、Object. hashCode()和Thread.run()。这些方法是很重要的目标,因为它们通常是在幕后被使用,很可能以不容易辨别的方式与其他组件进行交互。
通过提供覆盖的实现,攻击者可以使用不可信的代码来收集敏感信息、运行任意代码,或者发起拒绝服务攻击。
关于覆盖Object.clone()方法的更多详细信息参见指南10。
下面的违规代码示例展示了一个LicenseManager类,它维持着一个licenseMap。这个映射存储的是许可证类型(LicenseType)和许可证值对。
public class LicenseManager {
Map licenseMap =
new HashMap();
public LicenseManager() {
LicenseType type = new LicenseType();
type.setType("demo-license-key");
licenseMap.put(type, "ABC-DEF-PQR-XYZ");
}
public Object getLicenseKey(LicenseType licenseType) {
return licenseMap.get(licenseType);
}
public void setLicenseKey(LicenseType licenseType,
String licenseKey) {
licenseMap.put(licenseType, licenseKey);
}
}
class LicenseType {
private String type;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
@Override
public int hashCode() {
int res = 17;
res = res * 31 + type == null ? 0 : type.hashCode();
return res;
}
@Override
public boolean equals(Object arg) {
if (arg == null || !(arg instanceof LicenseType)) {
return false;
}
if (type.equals(((LicenseType) arg).getType())) {
return true;
}
return false;
}
}```
LicenseManager的构造函数用必须保持密文的演示许可证密钥,对licenseMap进行了初始化。为便于说明,许可证密钥是硬编码的;理想情况下应该从外部配置文件中读取经加密后存储的密钥。LicenseType类提供了equals()方法和hashCode()方法的覆盖实现。
这个实现是易受攻击的,攻击者可以扩展LicenseType类并覆盖equals()方法和hashCode()方法:
public class CraftedLicenseType extends LicenseType {
private static int guessedHashCode = 0;
@Override
public int hashCode() {
// Returns a new hashCode to test every time get() is called
guessedHashCode++;
return guessedHashCode;
}
@Override
public boolean equals(Object arg) {
// Always returns true
return true;
}
}`
下面是恶意的客户端程序:
public class DemoClient {
public static void main(String[] args) {
LicenseManager licenseManager = new LicenseManager();
for (int i = 0; i <= Integer.MAX_VALUE; i++) {
Object guessed =
licenseManager.getLicenseKey(new CraftedLicenseType());
if (guessed != null) {
// prints ABC-DEF-PQR-XYZ
System.out.println(guessed);
}
}
}
}```
客户端程序使用CraftedLicenseType类遍历所有可能的散列码序列,直到它成功匹配到存储在LicenseManager类中的演示许可证密钥对象的散列码。因此,仅仅只需几分钟,攻击者就可以发现licenseMap中的敏感数据。这个攻击是通过发现至少一个关于映射中键的散列冲突进行的。
####合规解决方案(IdentityHashMap)
下面的合规解决方案使用了一个IdentityHashMap来存储许可证信息,而不是HashMap。
public class LicenseManager {
Map licenseMap =
new IdentityHashMap();
// ...
}`
根据Java API中IdentityHashMap类的文档[API 2006]:
这个类以一个散列表实现Map(映射)接口,在比较键(和值)时使用引用相等代替对象相等。换句话说,如果在一个IdentityHashMap中有k1和k2两个键,那么当且仅当(k1==k2)时,才可以说它们是相等的。(而对于普通的Map实现(如HashMap)中的两个键k1和k2,当且仅当(k1==null ? k2==null : k1.equals(k2))时,才可以说它们是相等的。)
因此,覆盖方法不能暴露内部类的细节。客户端程序可以继续添加许可证密钥,甚至可以检索添加的键值对,如下列客户端代码所示。
public class DemoClient {
public static void main(String[] args) {
LicenseManager licenseManager = new LicenseManager();
LicenseType type = new LicenseType();
type.setType("custom-license-key");
licenseManager.setLicenseKey(type, "CUS-TOM-LIC-KEY");
Object licenseKeyValue = licenseManager.getLicenseKey(type);
// Prints CUS-TOM-LIC-KEY
System.out.println(licenseKeyValue);
}
}```
####合规解决方案(final类)
下面的合规解决方案将LicenseType类用final关键字声明成了不可更改的类,这样它的所有方法就都不能被覆盖了。
final class LicenseType {
// ...
}`
下面的违规代码示例包含一个Widget类和一个含有一组部件的LayoutManager类。
public class Widget {
private int noOfComponents;
public Widget(int noOfComponents) {
this.noOfComponents = noOfComponents;
}
public int getNoOfComponents() {
return noOfComponents;
}
public final void setNoOfComponents(int noOfComponents) {
this.noOfComponents = noOfComponents;
}
public boolean equals(Object o) {
if (o == null || !(o instanceof Widget)) {
return false;
}
Widget widget = (Widget) o;
return this.noOfComponents == widget.getNoOfComponents();
}
@Override
public int hashCode() {
int res = 31;
res = res * 17 + noOfComponents;
return res;
}
}
public class LayoutManager {
private Set layouts = new HashSet();
public void addWidget(Widget widget) {
if (!layouts.contains(widget)) {
layouts.add(widget);
}
}
public int getLayoutSize() {
return layouts.size();
}
}```
攻击者可以用Navigator部件扩展Widget类,并覆盖hashCode()方法:
public class Navigator extends Widget {
public Navigator(int noOfComponents) {
super(noOfComponents);
}
@Override
public int hashCode() {
int res = 31;
res = res * 17;
return res;
}
}`
客户端代码如下:
Widget nav = new Navigator(1);
Widget widget = new Widget(1);
LayoutManager manager = new LayoutManager();
manager.addWidget(nav);
manager.addWidget(widget);
System.out.println(manager.getLayoutSize()); // Prints 2```
layouts(布局)集合本应只包含一个条目,因为被添加的Navigator和Widget的组件数量都是1。然而,getLayoutSize()方法确返回了2。
产生这种差异的原因是,Widget的hashCode()方法只在Widget对象被添加到集合中时使用了一次。当添加Navigator时,集合使用的是Navigator类提供的hashCode()方法。因此,集合中包含两个不同的对象实例。
####合规解决方案(final类)
下面的合规解决方案将Widget类声明成final类,这样它的方法就不能被覆盖了。
public final class Widget {
// ...
}`
在下面的违规代码示例中,Worker类及其子类SubWorker,均包含一个用来启动一个线程的startThread()方法。
public class Worker implements Runnable {
Worker() { }
public void startThread(String name) {
new Thread(this, name).start();
}
@Override
public void run() {
System.out.println("Parent");
}
}
public class SubWorker extends Worker {
@Override
public void startThread(String name) {
super.startThread(name);
new Thread(this, name).start();
}
@Override
public void run() {
System.out.println("Child");
}
}```
如果一个客户端运行下面的代码:
Worker w = new SubWorker();
w.startThread("thread");`
客户端可能会希望Parent和Child都被打印出来。然而,Child会被打印两次,因为被覆盖的方法run()在启动一个新线程时被调用了两次。
下面的合规解决方案修改了SubWorkder类,移除了对super.startThread()的调用。
public class SubWorker extends Worker {
@Override
public void startThread(String name) {
new Thread(this, name).start();
}
// ...
}```
对客户端代码也做了修改,单独开启父线程和子线程。这个程序将会产生预期的输出:
Worker w1 = new Worker();
w1.startThread("parent-thread");
Worker w2 = new SubWorker();
w2.startThread("child-thread");