《Java编码指南:编写安全可靠程序的75条建议》—— 指南15:不要依赖可以被不可信代码覆盖的方法

简介: 不可信代码可以滥用可信代码提供的API来覆盖一些方法,如Object.equals()、Object. hashCode()和Thread.run()。这些方法是很重要的目标,因为它们通常是在幕后被使用,很可能以不容易辨别的方式与其他组件进行交互。

本节书摘来异步社区《Java编码指南:编写安全可靠程序的75条建议》一书中的第1章,第1.15节,作者:【美】Fred Long(弗雷德•朗), Dhruv Mohindra(德鲁•莫欣达), Robert C.Seacord(罗伯特 C.西科德), Dean F.Sutherland(迪恩 F.萨瑟兰), David Svoboda(大卫•斯沃博达),更多章节内容可以访问云栖社区“异步社区”公众号查看。

指南15:不要依赖可以被不可信代码覆盖的方法

不可信代码可以滥用可信代码提供的API来覆盖一些方法,如Object.equals()、Object. hashCode()和Thread.run()。这些方法是很重要的目标,因为它们通常是在幕后被使用,很可能以不容易辨别的方式与其他组件进行交互。

通过提供覆盖的实现,攻击者可以使用不可信的代码来收集敏感信息、运行任意代码,或者发起拒绝服务攻击。

关于覆盖Object.clone()方法的更多详细信息参见指南10。

违规代码示例(hashCode)

下面的违规代码示例展示了一个LicenseManager类,它维持着一个licenseMap。这个映射存储的是许可证类型(LicenseType)和许可证值对。

public class LicenseManager {
 Map<LicenseType, String> licenseMap =
  new HashMap<LicenseType, String>();

 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<Widget> layouts = new HashSet<Widget>();
 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 {
 // ...
}`

违规代码示例(run())

在下面的违规代码示例中,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");

相关文章
|
7天前
|
Java 测试技术 应用服务中间件
常见 Java 代码缺陷及规避方式(下)
常见 Java 代码缺陷及规避方式(下)
27 0
|
1天前
|
Java
Java接口中可以定义哪些方法?
【4月更文挑战第13天】
4 0
Java接口中可以定义哪些方法?
|
7天前
|
Java API
编码的奇迹:Java 21引入有序集合,数据结构再进化
编码的奇迹:Java 21引入有序集合,数据结构再进化
14 0
|
7天前
|
Java Shell
Java 21颠覆传统:未命名类与实例Main方法的编码变革
Java 21颠覆传统:未命名类与实例Main方法的编码变革
10 0
|
7天前
|
Java
代码的魔法师:Java反射工厂模式详解
代码的魔法师:Java反射工厂模式详解
18 0
|
7天前
|
监控 安全 Java
常见 Java 代码缺陷及规避方式(中)
常见 Java 代码缺陷及规避方式(中)
22 1
|
7天前
|
Java 应用服务中间件 Maven
使用IDEA搭建SpringMVC环境,Maven导入了依赖,但是运行报错 java.lang.ClassNotFoundException
使用IDEA搭建SpringMVC环境,Maven导入了依赖,但是运行报错 java.lang.ClassNotFoundException
8 1
|
9天前
|
Java
Java中关于ConditionObject的signal()方法的分析
Java中关于ConditionObject的signal()方法的分析
21 4
|
8月前
|
XML 监控 Java
《Java程序性能优化 让你的Java程序更快、更稳定》阅读笔记
《Java程序性能优化 让你的Java程序更快、更稳定》阅读笔记
|
Java
Java程序性能优化23
与一个接口 进行instanceof操作
1071 0