本节书摘来异步社区《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<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 { // ...}`
在下面的违规代码示例中,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");
相关资源:敏捷开发V1.0.pptx